consensus-cli 0.1.0 → 0.1.4
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/CHANGELOG.md +21 -0
- package/README.md +7 -2
- package/dist/activity.js +20 -3
- package/dist/cli.js +12 -0
- package/dist/codexLogs.js +73 -1
- package/dist/opencodeApi.js +84 -0
- package/dist/opencodeEvents.js +359 -0
- package/dist/opencodeServer.js +91 -0
- package/dist/opencodeStorage.js +127 -0
- package/dist/scan.js +333 -13
- package/package.json +3 -2
- package/public/app.js +162 -25
- package/public/index.html +3 -0
- package/public/style.css +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ This project follows Semantic Versioning.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.1.4 - 2026-01-24
|
|
9
|
+
- Fix OpenCode event tracking build error (pid activity typing).
|
|
10
|
+
|
|
11
|
+
## 0.1.3 - 2026-01-24
|
|
12
|
+
- Add OpenCode integration (API sessions, event stream, storage fallback).
|
|
13
|
+
- Autostart OpenCode server with opt-out and CLI flags.
|
|
14
|
+
- Split servers into a dedicated lane with distinct palette.
|
|
15
|
+
- Improve layout keys to prevent tile overlap.
|
|
16
|
+
- Add OpenCode unit/integration tests and configuration docs.
|
|
17
|
+
|
|
18
|
+
## 0.1.2 - 2026-01-24
|
|
19
|
+
- Lower CPU threshold for active detection.
|
|
20
|
+
- Increase activity window defaults for long-running turns.
|
|
21
|
+
- Skip vendor codex helper processes to avoid duplicate tiles.
|
|
22
|
+
- Improve session mapping for active-state detection.
|
|
23
|
+
|
|
24
|
+
## 0.1.1 - 2026-01-24
|
|
25
|
+
- Smooth active state to prevent animation flicker.
|
|
26
|
+
- Add `consensus-cli` binary alias so `npx consensus-cli` works.
|
|
27
|
+
- Extend active window to match Codex event cadence.
|
|
28
|
+
|
|
8
29
|
## 0.1.0 - 2026-01-24
|
|
9
30
|
- Initial public release.
|
|
10
31
|
- Improve work summaries and recent events (latest-first, event-only fallback).
|
package/README.md
CHANGED
|
@@ -62,10 +62,15 @@ consensus dev server running on http://127.0.0.1:8787
|
|
|
62
62
|
- `CONSENSUS_PORT`: server port (default `8787`).
|
|
63
63
|
- `CONSENSUS_POLL_MS`: polling interval in ms (default `1000`).
|
|
64
64
|
- `CONSENSUS_CODEX_HOME`: override Codex home (default `~/.codex`).
|
|
65
|
+
- `CONSENSUS_OPENCODE_HOST`: OpenCode server host (default `127.0.0.1`).
|
|
66
|
+
- `CONSENSUS_OPENCODE_PORT`: OpenCode server port (default `4096`).
|
|
67
|
+
- `CONSENSUS_OPENCODE_AUTOSTART`: set to `0` to disable OpenCode autostart.
|
|
68
|
+
- `CONSENSUS_OPENCODE_EVENTS`: set to `0` to disable OpenCode event stream.
|
|
65
69
|
- `CONSENSUS_PROCESS_MATCH`: regex to match codex processes.
|
|
66
70
|
- `CONSENSUS_REDACT_PII`: set to `0` to disable redaction (default enabled).
|
|
67
|
-
- `CONSENSUS_EVENT_ACTIVE_MS`: active window after last event in ms (default `
|
|
68
|
-
- `CONSENSUS_CPU_ACTIVE`: CPU threshold for active state (default `
|
|
71
|
+
- `CONSENSUS_EVENT_ACTIVE_MS`: active window after last event in ms (default `300000`).
|
|
72
|
+
- `CONSENSUS_CPU_ACTIVE`: CPU threshold for active state (default `1`).
|
|
73
|
+
- `CONSENSUS_ACTIVE_HOLD_MS`: keep active state this long after activity (default `600000`).
|
|
69
74
|
|
|
70
75
|
Full config details: `docs/configuration.md`
|
|
71
76
|
|
package/dist/activity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const DEFAULT_CPU_THRESHOLD =
|
|
2
|
-
const DEFAULT_EVENT_WINDOW_MS =
|
|
1
|
+
const DEFAULT_CPU_THRESHOLD = 1;
|
|
2
|
+
const DEFAULT_EVENT_WINDOW_MS = 300000;
|
|
3
|
+
const DEFAULT_ACTIVE_HOLD_MS = 600000;
|
|
3
4
|
export function deriveState(input) {
|
|
4
5
|
if (input.hasError)
|
|
5
6
|
return "error";
|
|
@@ -10,5 +11,21 @@ export function deriveState(input) {
|
|
|
10
11
|
const cpuActive = input.cpu > cpuThreshold;
|
|
11
12
|
const eventActive = typeof input.lastEventAt === "number" &&
|
|
12
13
|
now - input.lastEventAt <= eventWindowMs;
|
|
13
|
-
|
|
14
|
+
const inFlight = !!input.inFlight;
|
|
15
|
+
return cpuActive || eventActive || inFlight ? "active" : "idle";
|
|
16
|
+
}
|
|
17
|
+
export function deriveStateWithHold(input) {
|
|
18
|
+
const now = input.now ?? Date.now();
|
|
19
|
+
const holdMs = input.holdMs ?? Number(process.env.CONSENSUS_ACTIVE_HOLD_MS || DEFAULT_ACTIVE_HOLD_MS);
|
|
20
|
+
const baseState = deriveState({ ...input, now });
|
|
21
|
+
let lastActiveAt = input.previousActiveAt;
|
|
22
|
+
if (baseState === "active") {
|
|
23
|
+
lastActiveAt = now;
|
|
24
|
+
}
|
|
25
|
+
if (baseState === "idle" &&
|
|
26
|
+
typeof lastActiveAt === "number" &&
|
|
27
|
+
now - lastActiveAt <= holdMs) {
|
|
28
|
+
return { state: "active", lastActiveAt };
|
|
29
|
+
}
|
|
30
|
+
return { state: baseState, lastActiveAt };
|
|
14
31
|
}
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,9 @@ function printHelp() {
|
|
|
27
27
|
process.stdout.write(` --port <port> Port (default 8787)\n`);
|
|
28
28
|
process.stdout.write(` --poll <ms> Poll interval in ms\n`);
|
|
29
29
|
process.stdout.write(` --codex-home <path> Override CODEX_HOME\n`);
|
|
30
|
+
process.stdout.write(` --opencode-host <h> OpenCode host (default 127.0.0.1)\n`);
|
|
31
|
+
process.stdout.write(` --opencode-port <p> OpenCode port (default 4096)\n`);
|
|
32
|
+
process.stdout.write(` --no-opencode-autostart Disable OpenCode server autostart\n`);
|
|
30
33
|
process.stdout.write(` --process-match <re> Regex for process matching\n`);
|
|
31
34
|
process.stdout.write(` --no-redact Disable PII redaction\n`);
|
|
32
35
|
process.stdout.write(` -h, --help Show help\n`);
|
|
@@ -40,6 +43,9 @@ const host = readArg("--host");
|
|
|
40
43
|
const port = readArg("--port");
|
|
41
44
|
const poll = readArg("--poll");
|
|
42
45
|
const codexHome = readArg("--codex-home");
|
|
46
|
+
const opencodeHost = readArg("--opencode-host");
|
|
47
|
+
const opencodePort = readArg("--opencode-port");
|
|
48
|
+
const noOpenCodeAutostart = hasFlag("--no-opencode-autostart");
|
|
43
49
|
const match = readArg("--process-match");
|
|
44
50
|
const noRedact = hasFlag("--no-redact");
|
|
45
51
|
if (host)
|
|
@@ -50,6 +56,12 @@ if (poll)
|
|
|
50
56
|
env.CONSENSUS_POLL_MS = poll;
|
|
51
57
|
if (codexHome)
|
|
52
58
|
env.CONSENSUS_CODEX_HOME = codexHome;
|
|
59
|
+
if (opencodeHost)
|
|
60
|
+
env.CONSENSUS_OPENCODE_HOST = opencodeHost;
|
|
61
|
+
if (opencodePort)
|
|
62
|
+
env.CONSENSUS_OPENCODE_PORT = opencodePort;
|
|
63
|
+
if (noOpenCodeAutostart)
|
|
64
|
+
env.CONSENSUS_OPENCODE_AUTOSTART = "0";
|
|
53
65
|
if (match)
|
|
54
66
|
env.CONSENSUS_PROCESS_MATCH = match;
|
|
55
67
|
if (noRedact)
|
package/dist/codexLogs.js
CHANGED
|
@@ -4,11 +4,14 @@ import path from "path";
|
|
|
4
4
|
import { redactText } from "./redact.js";
|
|
5
5
|
const SESSION_WINDOW_MS = 30 * 60 * 1000;
|
|
6
6
|
const SESSION_SCAN_INTERVAL_MS = 5000;
|
|
7
|
+
const SESSION_ID_SCAN_INTERVAL_MS = 60000;
|
|
7
8
|
const MAX_READ_BYTES = 512 * 1024;
|
|
8
9
|
const MAX_EVENTS = 50;
|
|
9
10
|
let cachedSessions = [];
|
|
10
11
|
let lastSessionScan = 0;
|
|
11
12
|
const tailStates = new Map();
|
|
13
|
+
const sessionIdCache = new Map();
|
|
14
|
+
const sessionIdLastScan = new Map();
|
|
12
15
|
export function resolveCodexHome(env = process.env) {
|
|
13
16
|
const override = env.CONSENSUS_CODEX_HOME || env.CODEX_HOME;
|
|
14
17
|
return override ? path.resolve(override) : path.join(os.homedir(), ".codex");
|
|
@@ -41,6 +44,36 @@ async function walk(dir, out, windowMs) {
|
|
|
41
44
|
}
|
|
42
45
|
}));
|
|
43
46
|
}
|
|
47
|
+
async function findSessionFile(dir, sessionId) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
const found = await findSessionFile(fullPath, sessionId);
|
|
59
|
+
if (found)
|
|
60
|
+
return found;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
64
|
+
continue;
|
|
65
|
+
if (!entry.name.includes(sessionId))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const stat = await fsp.stat(fullPath);
|
|
69
|
+
return { path: fullPath, mtimeMs: stat.mtimeMs };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
44
77
|
export async function listRecentSessions(codexHome, windowMs = SESSION_WINDOW_MS) {
|
|
45
78
|
const now = Date.now();
|
|
46
79
|
if (now - lastSessionScan < SESSION_SCAN_INTERVAL_MS) {
|
|
@@ -54,6 +87,28 @@ export async function listRecentSessions(codexHome, windowMs = SESSION_WINDOW_MS
|
|
|
54
87
|
cachedSessions = results;
|
|
55
88
|
return results;
|
|
56
89
|
}
|
|
90
|
+
export async function findSessionById(codexHome, sessionId) {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const lastScan = sessionIdLastScan.get(sessionId) || 0;
|
|
93
|
+
if (now - lastScan < SESSION_ID_SCAN_INTERVAL_MS) {
|
|
94
|
+
const cached = sessionIdCache.get(sessionId);
|
|
95
|
+
if (cached) {
|
|
96
|
+
try {
|
|
97
|
+
const stat = await fsp.stat(cached);
|
|
98
|
+
return { path: cached, mtimeMs: stat.mtimeMs };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
sessionIdLastScan.set(sessionId, now);
|
|
107
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
108
|
+
const found = await findSessionFile(sessionsDir, sessionId);
|
|
109
|
+
sessionIdCache.set(sessionId, found ? found.path : null);
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
57
112
|
export function pickSessionForProcess(sessions, startTimeMs) {
|
|
58
113
|
if (sessions.length === 0)
|
|
59
114
|
return undefined;
|
|
@@ -256,6 +311,8 @@ export async function updateTail(sessionPath) {
|
|
|
256
311
|
const combined = state.partial + text;
|
|
257
312
|
const lines = combined.split(/\r?\n/);
|
|
258
313
|
state.partial = lines.pop() || "";
|
|
314
|
+
const startRe = /(turn|item|response)\.started/i;
|
|
315
|
+
const endRe = /(turn|item|response)\.(completed|failed|errored)/i;
|
|
259
316
|
for (const line of lines) {
|
|
260
317
|
if (!line.trim())
|
|
261
318
|
continue;
|
|
@@ -270,6 +327,12 @@ export async function updateTail(sessionPath) {
|
|
|
270
327
|
const { summary, kind, isError, model, type } = summarizeEvent(ev);
|
|
271
328
|
if (model)
|
|
272
329
|
state.model = model;
|
|
330
|
+
if (typeof type === "string") {
|
|
331
|
+
if (startRe.test(type))
|
|
332
|
+
state.inFlight = true;
|
|
333
|
+
if (endRe.test(type))
|
|
334
|
+
state.inFlight = false;
|
|
335
|
+
}
|
|
273
336
|
if (summary) {
|
|
274
337
|
const entry = {
|
|
275
338
|
ts,
|
|
@@ -328,5 +391,14 @@ export function summarizeTail(state) {
|
|
|
328
391
|
lastTool: state.lastTool?.summary,
|
|
329
392
|
lastPrompt: state.lastPrompt?.summary,
|
|
330
393
|
};
|
|
331
|
-
return {
|
|
394
|
+
return {
|
|
395
|
+
doing,
|
|
396
|
+
title,
|
|
397
|
+
events,
|
|
398
|
+
model: state.model,
|
|
399
|
+
hasError,
|
|
400
|
+
summary,
|
|
401
|
+
lastEventAt,
|
|
402
|
+
inFlight: state.inFlight,
|
|
403
|
+
};
|
|
332
404
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function shouldWarn(options) {
|
|
2
|
+
return options?.silent ? false : true;
|
|
3
|
+
}
|
|
4
|
+
export async function getOpenCodeSessions(host = "localhost", port = 4096, options) {
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
7
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
8
|
+
const warn = shouldWarn(options);
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(`http://${host}:${port}/session`, {
|
|
11
|
+
headers: {
|
|
12
|
+
Accept: "application/json",
|
|
13
|
+
"User-Agent": "consensus-scanner",
|
|
14
|
+
},
|
|
15
|
+
signal: controller.signal,
|
|
16
|
+
});
|
|
17
|
+
clearTimeout(timeoutId);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
if (warn) {
|
|
20
|
+
console.warn(`OpenCode API error: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return { ok: false, sessions: [], status: response.status, reachable: true };
|
|
23
|
+
}
|
|
24
|
+
const contentType = response.headers.get("content-type") || "";
|
|
25
|
+
if (!contentType.includes("json")) {
|
|
26
|
+
if (warn) {
|
|
27
|
+
console.warn(`OpenCode API non-JSON response (${contentType || "unknown"})`);
|
|
28
|
+
}
|
|
29
|
+
return { ok: false, sessions: [], status: response.status, reachable: true, error: "non_json" };
|
|
30
|
+
}
|
|
31
|
+
const payload = await response.json();
|
|
32
|
+
if (Array.isArray(payload))
|
|
33
|
+
return { ok: true, sessions: payload, reachable: true };
|
|
34
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.sessions)) {
|
|
35
|
+
return { ok: true, sessions: payload.sessions, reachable: true };
|
|
36
|
+
}
|
|
37
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.data)) {
|
|
38
|
+
return { ok: true, sessions: payload.data, reachable: true };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, sessions: [], reachable: true };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
if (warn) {
|
|
45
|
+
console.warn("Failed to fetch OpenCode sessions:", error);
|
|
46
|
+
}
|
|
47
|
+
const errorCode = typeof error?.cause?.code === "string"
|
|
48
|
+
? error.cause.code
|
|
49
|
+
: typeof error?.code === "string"
|
|
50
|
+
? error.code
|
|
51
|
+
: undefined;
|
|
52
|
+
return { ok: false, sessions: [], error: errorCode, reachable: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function getOpenCodeSession(sessionId, host = "localhost", port = 4096, options) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
58
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
const warn = shouldWarn(options);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`http://${host}:${port}/session/${sessionId}`, {
|
|
62
|
+
headers: {
|
|
63
|
+
Accept: "application/json",
|
|
64
|
+
"User-Agent": "consensus-scanner",
|
|
65
|
+
},
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
if (warn) {
|
|
71
|
+
console.warn(`OpenCode API error for session ${sessionId}: ${response.status} ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return await response.json();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
if (warn) {
|
|
80
|
+
console.warn(`Failed to fetch OpenCode session ${sessionId}:`, error);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { redactText } from "./redact.js";
|
|
2
|
+
const MAX_EVENTS = 50;
|
|
3
|
+
const STALE_TTL_MS = 30 * 60 * 1000;
|
|
4
|
+
const RECONNECT_MIN_MS = 10000;
|
|
5
|
+
const sessionActivity = new Map();
|
|
6
|
+
const pidActivity = new Map();
|
|
7
|
+
let connecting = false;
|
|
8
|
+
let connected = false;
|
|
9
|
+
let lastConnectAt = 0;
|
|
10
|
+
let lastFailureAt = 0;
|
|
11
|
+
function nowMs() {
|
|
12
|
+
return Date.now();
|
|
13
|
+
}
|
|
14
|
+
function parseTimestamp(value) {
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
16
|
+
return value < 100000000000 ? value * 1000 : value;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
const parsed = Date.parse(value);
|
|
20
|
+
if (!Number.isNaN(parsed))
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
return nowMs();
|
|
24
|
+
}
|
|
25
|
+
function extractText(value) {
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return value;
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map(extractText).filter(Boolean).join(" ");
|
|
30
|
+
if (value && typeof value === "object") {
|
|
31
|
+
if (typeof value.text === "string")
|
|
32
|
+
return value.text;
|
|
33
|
+
if (typeof value.content === "string")
|
|
34
|
+
return value.content;
|
|
35
|
+
if (typeof value.message === "string")
|
|
36
|
+
return value.message;
|
|
37
|
+
if (value.message && typeof value.message.content === "string") {
|
|
38
|
+
return value.message.content;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function getSessionId(raw) {
|
|
44
|
+
return (raw?.sessionId ||
|
|
45
|
+
raw?.session_id ||
|
|
46
|
+
raw?.session?.id ||
|
|
47
|
+
raw?.session?.sessionId ||
|
|
48
|
+
raw?.properties?.sessionId ||
|
|
49
|
+
raw?.properties?.session_id);
|
|
50
|
+
}
|
|
51
|
+
function getPid(raw) {
|
|
52
|
+
const pid = raw?.pid ||
|
|
53
|
+
raw?.process?.pid ||
|
|
54
|
+
raw?.properties?.pid ||
|
|
55
|
+
raw?.properties?.processId;
|
|
56
|
+
if (typeof pid === "number" && Number.isFinite(pid))
|
|
57
|
+
return pid;
|
|
58
|
+
if (typeof pid === "string") {
|
|
59
|
+
const parsed = Number(pid);
|
|
60
|
+
if (!Number.isNaN(parsed))
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
function summarizeEvent(raw) {
|
|
66
|
+
const typeRaw = raw?.type ||
|
|
67
|
+
raw?.event ||
|
|
68
|
+
raw?.name ||
|
|
69
|
+
raw?.kind ||
|
|
70
|
+
raw?.properties?.type ||
|
|
71
|
+
"event";
|
|
72
|
+
const type = typeof typeRaw === "string" ? typeRaw : "event";
|
|
73
|
+
const lowerType = type.toLowerCase();
|
|
74
|
+
const status = raw?.status || raw?.state || raw?.properties?.status;
|
|
75
|
+
const statusStr = typeof status === "string" ? status.toLowerCase() : "";
|
|
76
|
+
const isError = !!raw?.error || lowerType.includes("error") || statusStr.includes("error");
|
|
77
|
+
let inFlight;
|
|
78
|
+
if (lowerType.includes("started") ||
|
|
79
|
+
statusStr.includes("started") ||
|
|
80
|
+
statusStr.includes("running") ||
|
|
81
|
+
statusStr.includes("processing") ||
|
|
82
|
+
statusStr.includes("in_progress")) {
|
|
83
|
+
inFlight = true;
|
|
84
|
+
}
|
|
85
|
+
else if (lowerType.includes("completed") ||
|
|
86
|
+
lowerType.includes("finished") ||
|
|
87
|
+
lowerType.includes("done") ||
|
|
88
|
+
lowerType.includes("ended") ||
|
|
89
|
+
statusStr.includes("completed") ||
|
|
90
|
+
statusStr.includes("finished") ||
|
|
91
|
+
statusStr.includes("done") ||
|
|
92
|
+
statusStr.includes("ended") ||
|
|
93
|
+
statusStr.includes("idle") ||
|
|
94
|
+
statusStr.includes("stopped") ||
|
|
95
|
+
statusStr.includes("paused") ||
|
|
96
|
+
isError) {
|
|
97
|
+
inFlight = false;
|
|
98
|
+
}
|
|
99
|
+
if (lowerType.includes("compaction")) {
|
|
100
|
+
const phase = statusStr || raw?.phase || raw?.properties?.phase;
|
|
101
|
+
const summary = phase ? `compaction: ${phase}` : "compaction";
|
|
102
|
+
return { summary, kind: "other", isError, type, inFlight };
|
|
103
|
+
}
|
|
104
|
+
const cmd = raw?.command ||
|
|
105
|
+
raw?.cmd ||
|
|
106
|
+
raw?.input?.command ||
|
|
107
|
+
raw?.input?.cmd ||
|
|
108
|
+
raw?.properties?.command ||
|
|
109
|
+
raw?.properties?.cmd ||
|
|
110
|
+
(Array.isArray(raw?.args) ? raw.args.join(" ") : undefined);
|
|
111
|
+
if (typeof cmd === "string" && cmd.trim()) {
|
|
112
|
+
const summary = redactText(`cmd: ${cmd.trim()}`) || `cmd: ${cmd.trim()}`;
|
|
113
|
+
return { summary, kind: "command", isError, type, inFlight };
|
|
114
|
+
}
|
|
115
|
+
const pathHint = raw?.path ||
|
|
116
|
+
raw?.file ||
|
|
117
|
+
raw?.filename ||
|
|
118
|
+
raw?.target ||
|
|
119
|
+
raw?.properties?.path ||
|
|
120
|
+
raw?.properties?.file;
|
|
121
|
+
if (typeof pathHint === "string" && pathHint.trim() && lowerType.includes("file")) {
|
|
122
|
+
const summary = redactText(`edit: ${pathHint.trim()}`) || `edit: ${pathHint.trim()}`;
|
|
123
|
+
return { summary, kind: "edit", isError, type };
|
|
124
|
+
}
|
|
125
|
+
const tool = raw?.tool ||
|
|
126
|
+
raw?.tool_name ||
|
|
127
|
+
raw?.toolName ||
|
|
128
|
+
raw?.properties?.tool ||
|
|
129
|
+
raw?.properties?.tool_name;
|
|
130
|
+
if (typeof tool === "string" && tool.trim() && lowerType.includes("tool")) {
|
|
131
|
+
const summary = redactText(`tool: ${tool.trim()}`) || `tool: ${tool.trim()}`;
|
|
132
|
+
return { summary, kind: "tool", isError, type, inFlight };
|
|
133
|
+
}
|
|
134
|
+
const promptText = extractText(raw?.prompt) ||
|
|
135
|
+
extractText(raw?.input) ||
|
|
136
|
+
extractText(raw?.instruction) ||
|
|
137
|
+
extractText(raw?.properties?.prompt);
|
|
138
|
+
if (promptText && lowerType.includes("prompt")) {
|
|
139
|
+
const trimmed = promptText.replace(/\s+/g, " ").trim();
|
|
140
|
+
const snippet = trimmed.slice(0, 120);
|
|
141
|
+
const summary = redactText(`prompt: ${snippet}`) || `prompt: ${snippet}`;
|
|
142
|
+
return { summary, kind: "prompt", isError, type };
|
|
143
|
+
}
|
|
144
|
+
const messageText = extractText(raw?.message) ||
|
|
145
|
+
extractText(raw?.content) ||
|
|
146
|
+
extractText(raw?.text) ||
|
|
147
|
+
extractText(raw?.properties?.message);
|
|
148
|
+
if (messageText) {
|
|
149
|
+
const trimmed = messageText.replace(/\s+/g, " ").trim();
|
|
150
|
+
const snippet = trimmed.slice(0, 80);
|
|
151
|
+
const summary = redactText(snippet) || snippet;
|
|
152
|
+
return { summary, kind: "message", isError, type };
|
|
153
|
+
}
|
|
154
|
+
if (type && type !== "event") {
|
|
155
|
+
const summary = redactText(`event: ${type}`) || `event: ${type}`;
|
|
156
|
+
return { summary, kind: "other", isError, type };
|
|
157
|
+
}
|
|
158
|
+
return { kind: "other", isError, type };
|
|
159
|
+
}
|
|
160
|
+
function ensureActivity(key, map, now) {
|
|
161
|
+
const existing = map.get(key);
|
|
162
|
+
if (existing) {
|
|
163
|
+
existing.lastSeenAt = now;
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
const fresh = {
|
|
167
|
+
events: [],
|
|
168
|
+
summary: {},
|
|
169
|
+
lastSeenAt: now,
|
|
170
|
+
};
|
|
171
|
+
map.set(key, fresh);
|
|
172
|
+
return fresh;
|
|
173
|
+
}
|
|
174
|
+
function recordEvent(state, entry, kind) {
|
|
175
|
+
state.events.push(entry);
|
|
176
|
+
if (state.events.length > MAX_EVENTS) {
|
|
177
|
+
state.events = state.events.slice(-MAX_EVENTS);
|
|
178
|
+
}
|
|
179
|
+
state.lastEventAt = Math.max(state.lastEventAt || 0, entry.ts);
|
|
180
|
+
if (kind === "command")
|
|
181
|
+
state.lastCommand = entry;
|
|
182
|
+
if (kind === "edit")
|
|
183
|
+
state.lastEdit = entry;
|
|
184
|
+
if (kind === "message")
|
|
185
|
+
state.lastMessage = entry;
|
|
186
|
+
if (kind === "tool")
|
|
187
|
+
state.lastTool = entry;
|
|
188
|
+
if (kind === "prompt")
|
|
189
|
+
state.lastPrompt = entry;
|
|
190
|
+
if (entry.isError)
|
|
191
|
+
state.lastError = entry;
|
|
192
|
+
state.summary = {
|
|
193
|
+
current: state.events[state.events.length - 1]?.summary,
|
|
194
|
+
lastCommand: state.lastCommand?.summary,
|
|
195
|
+
lastEdit: state.lastEdit?.summary,
|
|
196
|
+
lastMessage: state.lastMessage?.summary,
|
|
197
|
+
lastTool: state.lastTool?.summary,
|
|
198
|
+
lastPrompt: state.lastPrompt?.summary,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function handleRawEvent(raw) {
|
|
202
|
+
const ts = parseTimestamp(raw?.ts ||
|
|
203
|
+
raw?.timestamp ||
|
|
204
|
+
raw?.time ||
|
|
205
|
+
raw?.created_at ||
|
|
206
|
+
raw?.createdAt ||
|
|
207
|
+
raw?.properties?.time ||
|
|
208
|
+
raw?.properties?.timestamp);
|
|
209
|
+
const sessionId = getSessionId(raw);
|
|
210
|
+
const pid = getPid(raw);
|
|
211
|
+
const { summary, kind, isError, type, inFlight } = summarizeEvent(raw);
|
|
212
|
+
if (!summary)
|
|
213
|
+
return;
|
|
214
|
+
const entry = {
|
|
215
|
+
ts,
|
|
216
|
+
type: typeof type === "string" ? type : "event",
|
|
217
|
+
summary,
|
|
218
|
+
isError,
|
|
219
|
+
};
|
|
220
|
+
const now = nowMs();
|
|
221
|
+
if (sessionId) {
|
|
222
|
+
const state = ensureActivity(sessionId, sessionActivity, now);
|
|
223
|
+
recordEvent(state, entry, kind);
|
|
224
|
+
if (typeof inFlight === "boolean")
|
|
225
|
+
state.inFlight = inFlight;
|
|
226
|
+
}
|
|
227
|
+
if (typeof pid === "number") {
|
|
228
|
+
const state = ensureActivity(pid, pidActivity, now);
|
|
229
|
+
recordEvent(state, entry, kind);
|
|
230
|
+
if (typeof inFlight === "boolean")
|
|
231
|
+
state.inFlight = inFlight;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function pruneStale() {
|
|
235
|
+
const cutoff = nowMs() - STALE_TTL_MS;
|
|
236
|
+
for (const [key, state] of sessionActivity.entries()) {
|
|
237
|
+
if (state.lastSeenAt < cutoff)
|
|
238
|
+
sessionActivity.delete(key);
|
|
239
|
+
}
|
|
240
|
+
for (const [key, state] of pidActivity.entries()) {
|
|
241
|
+
if (state.lastSeenAt < cutoff)
|
|
242
|
+
pidActivity.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function connectStream(host, port) {
|
|
246
|
+
connecting = true;
|
|
247
|
+
lastConnectAt = nowMs();
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(`http://${host}:${port}/global/event`, {
|
|
250
|
+
headers: {
|
|
251
|
+
Accept: "text/event-stream",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
if (!response.ok || !response.body) {
|
|
255
|
+
connected = false;
|
|
256
|
+
connecting = false;
|
|
257
|
+
lastFailureAt = nowMs();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
connected = true;
|
|
261
|
+
connecting = false;
|
|
262
|
+
const reader = response.body.getReader();
|
|
263
|
+
let buffer = "";
|
|
264
|
+
let currentEvent;
|
|
265
|
+
let dataLines = [];
|
|
266
|
+
while (true) {
|
|
267
|
+
const { value, done } = await reader.read();
|
|
268
|
+
if (done)
|
|
269
|
+
break;
|
|
270
|
+
buffer += Buffer.from(value).toString("utf8");
|
|
271
|
+
let idx;
|
|
272
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
273
|
+
const line = buffer.slice(0, idx).trimEnd();
|
|
274
|
+
buffer = buffer.slice(idx + 1);
|
|
275
|
+
if (!line) {
|
|
276
|
+
if (dataLines.length) {
|
|
277
|
+
const payload = dataLines.join("\n");
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(payload);
|
|
280
|
+
const raw = parsed?.payload ?? parsed;
|
|
281
|
+
if (currentEvent && typeof raw === "object" && !raw.type) {
|
|
282
|
+
raw.type = currentEvent;
|
|
283
|
+
}
|
|
284
|
+
if (parsed?.type && typeof raw === "object" && !raw.type) {
|
|
285
|
+
raw.type = parsed.type;
|
|
286
|
+
}
|
|
287
|
+
handleRawEvent(raw);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// ignore malformed payloads
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
currentEvent = undefined;
|
|
294
|
+
dataLines = [];
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (line.startsWith("event:")) {
|
|
298
|
+
currentEvent = line.slice(6).trim();
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (line.startsWith("data:")) {
|
|
302
|
+
dataLines.push(line.slice(5).trim());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
lastFailureAt = nowMs();
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
connected = false;
|
|
312
|
+
connecting = false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export function ensureOpenCodeEventStream(host, port) {
|
|
316
|
+
if (process.env.CONSENSUS_OPENCODE_EVENTS === "0")
|
|
317
|
+
return;
|
|
318
|
+
const now = nowMs();
|
|
319
|
+
if (connecting || connected)
|
|
320
|
+
return;
|
|
321
|
+
if (now - lastConnectAt < RECONNECT_MIN_MS)
|
|
322
|
+
return;
|
|
323
|
+
if (now - lastFailureAt < RECONNECT_MIN_MS)
|
|
324
|
+
return;
|
|
325
|
+
pruneStale();
|
|
326
|
+
void connectStream(host, port);
|
|
327
|
+
}
|
|
328
|
+
export function getOpenCodeActivityBySession(sessionId) {
|
|
329
|
+
if (!sessionId)
|
|
330
|
+
return null;
|
|
331
|
+
const state = sessionActivity.get(sessionId);
|
|
332
|
+
if (!state)
|
|
333
|
+
return null;
|
|
334
|
+
const events = state.events.slice(-20);
|
|
335
|
+
const hasError = !!state.lastError || events.some((ev) => ev.isError);
|
|
336
|
+
return {
|
|
337
|
+
events,
|
|
338
|
+
summary: state.summary,
|
|
339
|
+
lastEventAt: state.lastEventAt,
|
|
340
|
+
hasError,
|
|
341
|
+
inFlight: state.inFlight,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export function getOpenCodeActivityByPid(pid) {
|
|
345
|
+
if (typeof pid !== "number")
|
|
346
|
+
return null;
|
|
347
|
+
const state = pidActivity.get(pid);
|
|
348
|
+
if (!state)
|
|
349
|
+
return null;
|
|
350
|
+
const events = state.events.slice(-20);
|
|
351
|
+
const hasError = !!state.lastError || events.some((ev) => ev.isError);
|
|
352
|
+
return {
|
|
353
|
+
events,
|
|
354
|
+
summary: state.summary,
|
|
355
|
+
lastEventAt: state.lastEventAt,
|
|
356
|
+
hasError,
|
|
357
|
+
inFlight: state.inFlight,
|
|
358
|
+
};
|
|
359
|
+
}
|