@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vk-log-stream.mjs — Real-time agent log capture from Vibe-Kanban WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* Connects to VK's execution-process raw-logs WebSocket endpoints to capture
|
|
5
|
+
* agent stdout/stderr that would otherwise be invisible to the monitor.
|
|
6
|
+
*
|
|
7
|
+
* VK Architecture (reverse-engineered from BloopAI/vibe-kanban):
|
|
8
|
+
* - Each agent session creates an "execution process" inside VK
|
|
9
|
+
* - Agent stdout/stderr → MsgStore (in-memory broadcast channel)
|
|
10
|
+
* - MsgStore → raw-logs WebSocket endpoint for live streaming
|
|
11
|
+
* - MsgStore → SQLite (JSONL) for persistence
|
|
12
|
+
*
|
|
13
|
+
* VK API endpoints used:
|
|
14
|
+
* GET /api/execution-processes/{id} — get single process (REST)
|
|
15
|
+
* GET /api/execution-processes/{id}/raw-logs/ws — raw log stream (WebSocket)
|
|
16
|
+
* GET /api/execution-processes/stream/session/ws?session_id=X — process discovery per session (WebSocket, JSON Patch)
|
|
17
|
+
*
|
|
18
|
+
* Note: There is NO list-all endpoint (GET /api/execution-processes). Discovery
|
|
19
|
+
* must go through the session-based WebSocket stream or direct connectToProcess() calls.
|
|
20
|
+
*
|
|
21
|
+
* WebSocket message format (LogMsg enum variants):
|
|
22
|
+
* {"Stdout": "line of text"}
|
|
23
|
+
* {"Stderr": "line of text"}
|
|
24
|
+
* {"JsonPatch": [{"op": "add", "path": "...", "value": {...}}]}
|
|
25
|
+
* {"Finished": ""}
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* import { VkLogStream } from "./vk-log-stream.mjs";
|
|
29
|
+
* const stream = new VkLogStream(vkEndpointUrl, { logDir, onLine });
|
|
30
|
+
* stream.start();
|
|
31
|
+
* stream.connectToSession(sessionId); // discover processes via session stream
|
|
32
|
+
* stream.connectToProcess(processId); // direct connection to known process
|
|
33
|
+
* // ... later
|
|
34
|
+
* stream.stop();
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { resolve } from "node:path";
|
|
39
|
+
|
|
40
|
+
// ── Configuration defaults ──────────────────────────────────────────────────
|
|
41
|
+
const RECONNECT_DELAY_MS = 3000; // Delay before reconnecting a dropped WebSocket
|
|
42
|
+
const MAX_RECONNECT_ATTEMPTS = 10; // Max consecutive reconnect failures per process
|
|
43
|
+
const SESSION_RECONNECT_DELAY_MS = 5000; // Delay before reconnecting a dropped session stream
|
|
44
|
+
const MAX_SESSION_RECONNECT_ATTEMPTS = 15; // Max consecutive reconnect failures per session
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* VkLogStream - Captures real-time agent logs from VK execution processes.
|
|
48
|
+
*
|
|
49
|
+
* Discovery model:
|
|
50
|
+
* - No REST list endpoint exists; discovery uses the session-based WebSocket
|
|
51
|
+
* (stream/session/ws) which pushes JSON Patch updates as processes start/stop.
|
|
52
|
+
* - Monitor calls connectToSession(sessionId) when sessions are created.
|
|
53
|
+
* - Monitor calls connectToProcess(processId) when a specific process ID is known.
|
|
54
|
+
*/
|
|
55
|
+
export class VkLogStream {
|
|
56
|
+
/** @type {string} VK API base URL (e.g. http://192.168.0.161:54089) */
|
|
57
|
+
#baseUrl;
|
|
58
|
+
|
|
59
|
+
/** @type {string} WebSocket base URL (ws:// or wss://) */
|
|
60
|
+
#wsBaseUrl;
|
|
61
|
+
|
|
62
|
+
/** @type {string} Directory to write per-process log files */
|
|
63
|
+
#logDir;
|
|
64
|
+
|
|
65
|
+
/** @type {string|null} Directory for structured VK session logs (like codex-exec) */
|
|
66
|
+
#sessionLogDir;
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} Whether to echo log lines to console */
|
|
69
|
+
#echo;
|
|
70
|
+
|
|
71
|
+
/** @type {((line: string, meta: {processId: string, stream: string}) => boolean)|null} */
|
|
72
|
+
#filterLine;
|
|
73
|
+
|
|
74
|
+
/** @type {((line: string, meta: {processId: string, stream: string}) => void)|null} */
|
|
75
|
+
#onLine;
|
|
76
|
+
|
|
77
|
+
/** @type {Map<string, WebSocket>} Active raw-log WebSocket connections by process ID */
|
|
78
|
+
#connections = new Map();
|
|
79
|
+
|
|
80
|
+
/** @type {Map<string, number>} Reconnect attempt counts by process ID */
|
|
81
|
+
#reconnectCounts = new Map();
|
|
82
|
+
|
|
83
|
+
/** @type {Set<string>} Process IDs that have sent Finished */
|
|
84
|
+
#finished = new Set();
|
|
85
|
+
|
|
86
|
+
/** @type {boolean} Whether the stream is running */
|
|
87
|
+
#running = false;
|
|
88
|
+
|
|
89
|
+
/** @type {Set<string>} Known process IDs (to avoid re-connecting) */
|
|
90
|
+
#knownProcessIds = new Set();
|
|
91
|
+
|
|
92
|
+
/** @type {Map<string, WebSocket>} Session stream WebSocket connections by session ID */
|
|
93
|
+
#sessionStreams = new Map();
|
|
94
|
+
|
|
95
|
+
/** @type {Map<string, number>} Session stream reconnect attempt counts */
|
|
96
|
+
#sessionReconnectCounts = new Map();
|
|
97
|
+
|
|
98
|
+
/** @type {Set<string>} Session IDs we're tracking */
|
|
99
|
+
#trackedSessions = new Set();
|
|
100
|
+
|
|
101
|
+
/** @type {Map<string, object>} Per-process task metadata (attemptId, taskTitle, branch, etc.) */
|
|
102
|
+
#processMeta = new Map();
|
|
103
|
+
|
|
104
|
+
/** @type {Set<string>} Process IDs that already had their session-log header written */
|
|
105
|
+
#sessionLogHeaderWritten = new Set();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Callback invoked when a new execution process is discovered and connected.
|
|
109
|
+
* The monitor uses this to look up task metadata and call setProcessMeta().
|
|
110
|
+
* @type {((processId: string, meta: {sessionId?: string, runReason?: string}) => void)|null}
|
|
111
|
+
*/
|
|
112
|
+
#onProcessConnected;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} vkEndpointUrl - VK API base URL (e.g. http://127.0.0.1:54089)
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {string} [opts.logDir] - Directory for per-process log files
|
|
118
|
+
* @param {string} [opts.sessionLogDir] - Directory for structured session logs (codex-exec style)
|
|
119
|
+
* @param {boolean} [opts.echo=false] - Echo log lines to console
|
|
120
|
+
* @param {(line: string, meta: {processId: string, stream: string}) => boolean} [opts.filterLine] - Return false to drop noisy lines
|
|
121
|
+
* @param {(line: string, meta: {processId: string, stream: string}) => void} [opts.onLine] - Callback per log line
|
|
122
|
+
* @param {(processId: string, meta: {sessionId?: string}) => void} [opts.onProcessConnected] - Callback when new process discovered
|
|
123
|
+
*/
|
|
124
|
+
constructor(vkEndpointUrl, opts = {}) {
|
|
125
|
+
this.#baseUrl = vkEndpointUrl.replace(/\/+$/, "");
|
|
126
|
+
this.#wsBaseUrl = this.#baseUrl
|
|
127
|
+
.replace(/^http:/, "ws:")
|
|
128
|
+
.replace(/^https:/, "wss:");
|
|
129
|
+
this.#logDir = opts.logDir || null;
|
|
130
|
+
this.#sessionLogDir = opts.sessionLogDir || null;
|
|
131
|
+
this.#echo = opts.echo || false;
|
|
132
|
+
this.#filterLine = typeof opts.filterLine === "function" ? opts.filterLine : null;
|
|
133
|
+
this.#onLine = opts.onLine || null;
|
|
134
|
+
this.#onProcessConnected = opts.onProcessConnected || null;
|
|
135
|
+
|
|
136
|
+
if (this.#logDir) {
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(this.#logDir, { recursive: true });
|
|
139
|
+
} catch {
|
|
140
|
+
/* best effort */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (this.#sessionLogDir) {
|
|
144
|
+
try {
|
|
145
|
+
mkdirSync(this.#sessionLogDir, { recursive: true });
|
|
146
|
+
} catch {
|
|
147
|
+
/* best effort */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Set task metadata for a process, used to enrich structured session logs.
|
|
154
|
+
* Call this before or after connectToProcess/connectToSession — metadata
|
|
155
|
+
* will be applied whenever the first log line arrives.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} processId - The execution process UUID
|
|
158
|
+
* @param {object} meta - Task context metadata
|
|
159
|
+
* @param {string} [meta.attemptId] - VK attempt UUID
|
|
160
|
+
* @param {string} [meta.taskId] - VK task UUID
|
|
161
|
+
* @param {string} [meta.taskTitle] - Human-readable task title
|
|
162
|
+
* @param {string} [meta.branch] - Git branch name
|
|
163
|
+
* @param {string} [meta.sessionId] - VK session UUID
|
|
164
|
+
* @param {string} [meta.executor] - Executor type (e.g. "codex", "copilot")
|
|
165
|
+
* @param {string} [meta.executorVariant] - Executor variant
|
|
166
|
+
*/
|
|
167
|
+
setProcessMeta(processId, meta) {
|
|
168
|
+
if (!processId) return;
|
|
169
|
+
const existing = this.#processMeta.get(processId) || {};
|
|
170
|
+
this.#processMeta.set(processId, { ...existing, ...meta });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the structured session log path for a process.
|
|
175
|
+
* Lazily assigns a stable timestamp on first call per process.
|
|
176
|
+
* @param {string} processId
|
|
177
|
+
* @returns {string|null}
|
|
178
|
+
*/
|
|
179
|
+
getSessionLogPath(processId) {
|
|
180
|
+
if (!this.#sessionLogDir || !processId) return null;
|
|
181
|
+
const shortId = processId.slice(0, 8);
|
|
182
|
+
let meta = this.#processMeta.get(processId);
|
|
183
|
+
if (!meta) {
|
|
184
|
+
meta = {};
|
|
185
|
+
this.#processMeta.set(processId, meta);
|
|
186
|
+
}
|
|
187
|
+
if (!meta._logStamp) {
|
|
188
|
+
meta._logStamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
189
|
+
}
|
|
190
|
+
return resolve(
|
|
191
|
+
this.#sessionLogDir,
|
|
192
|
+
`vk-session-${meta._logStamp}-${shortId}.log`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Start the log stream manager (enables connections, no automatic polling).
|
|
198
|
+
* Call connectToSession() or connectToProcess() to actually capture logs.
|
|
199
|
+
*/
|
|
200
|
+
start() {
|
|
201
|
+
if (this.#running) return;
|
|
202
|
+
this.#running = true;
|
|
203
|
+
console.log(
|
|
204
|
+
`[vk-log-stream] started — ready for session/process connections (${this.#baseUrl})`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stop all connections (raw-log streams + session streams).
|
|
210
|
+
*/
|
|
211
|
+
stop() {
|
|
212
|
+
if (!this.#running) return;
|
|
213
|
+
this.#running = false;
|
|
214
|
+
|
|
215
|
+
// Close session stream WebSockets
|
|
216
|
+
for (const [id, ws] of this.#sessionStreams) {
|
|
217
|
+
try {
|
|
218
|
+
ws.close(1000, "monitor shutdown");
|
|
219
|
+
} catch {
|
|
220
|
+
/* best effort */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.#sessionStreams.clear();
|
|
224
|
+
this.#sessionReconnectCounts.clear();
|
|
225
|
+
this.#trackedSessions.clear();
|
|
226
|
+
|
|
227
|
+
// Close raw-log WebSockets
|
|
228
|
+
for (const [id, ws] of this.#connections) {
|
|
229
|
+
try {
|
|
230
|
+
ws.close(1000, "monitor shutdown");
|
|
231
|
+
} catch {
|
|
232
|
+
/* best effort */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.#connections.clear();
|
|
236
|
+
this.#reconnectCounts.clear();
|
|
237
|
+
console.log("[vk-log-stream] stopped");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Connect to a session's execution-process stream to auto-discover processes.
|
|
242
|
+
* VK endpoint: GET /api/execution-processes/stream/session/ws?session_id=X
|
|
243
|
+
*
|
|
244
|
+
* The server pushes JSON Patch updates as processes are created, updated, or
|
|
245
|
+
* completed. This method parses those patches to automatically connect to
|
|
246
|
+
* each running process's raw-logs WebSocket.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} sessionId - The VK session UUID
|
|
249
|
+
*/
|
|
250
|
+
connectToSession(sessionId) {
|
|
251
|
+
if (!sessionId || !this.#running) return;
|
|
252
|
+
if (this.#sessionStreams.has(sessionId)) return; // already connected
|
|
253
|
+
this.#trackedSessions.add(sessionId);
|
|
254
|
+
this.#openSessionStream(sessionId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Connect to a specific execution process's raw-logs WebSocket.
|
|
259
|
+
* @param {string} processId - The execution process UUID
|
|
260
|
+
* @param {object} [meta] - Optional metadata (task_id, branch, etc.)
|
|
261
|
+
*/
|
|
262
|
+
connectToProcess(processId, meta = {}) {
|
|
263
|
+
if (!processId || !this.#running) return;
|
|
264
|
+
if (this.#connections.has(processId) || this.#finished.has(processId)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.#knownProcessIds.add(processId);
|
|
268
|
+
this.#connectWebSocket(processId, meta);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get the set of currently connected process IDs.
|
|
273
|
+
* @returns {Set<string>}
|
|
274
|
+
*/
|
|
275
|
+
getActiveConnections() {
|
|
276
|
+
return new Set(this.#connections.keys());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get stream statistics.
|
|
281
|
+
* @returns {{ active: number, finished: number, known: number, sessions: number }}
|
|
282
|
+
*/
|
|
283
|
+
getStats() {
|
|
284
|
+
return {
|
|
285
|
+
active: this.#connections.size,
|
|
286
|
+
finished: this.#finished.size,
|
|
287
|
+
known: this.#knownProcessIds.size,
|
|
288
|
+
sessions: this.#sessionStreams.size,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Close the WebSocket for a specific process, effectively killing the log stream.
|
|
294
|
+
* @param {string} processId
|
|
295
|
+
* @param {string} [reason="killed by anomaly detector"]
|
|
296
|
+
* @returns {boolean} True if a connection was found and closed.
|
|
297
|
+
*/
|
|
298
|
+
killProcess(processId, reason = "killed by anomaly detector") {
|
|
299
|
+
const ws = this.#connections.get(processId);
|
|
300
|
+
if (!ws) return false;
|
|
301
|
+
const shortId = processId.slice(0, 8);
|
|
302
|
+
console.warn(`[vk-log-stream:${shortId}] killing process: ${reason}`);
|
|
303
|
+
// Mark as finished BEFORE closing so the close handler won't reconnect
|
|
304
|
+
this.#finished.add(processId);
|
|
305
|
+
try {
|
|
306
|
+
ws.close(1000, reason);
|
|
307
|
+
} catch {
|
|
308
|
+
/* best effort */
|
|
309
|
+
}
|
|
310
|
+
this.#connections.delete(processId);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Close a session stream WebSocket and stop tracking it.
|
|
317
|
+
* @param {string} sessionId
|
|
318
|
+
* @param {string} [reason="session no longer tracked"]
|
|
319
|
+
* @returns {boolean} True if the session was tracked.
|
|
320
|
+
*/
|
|
321
|
+
closeSession(sessionId, reason = "session no longer tracked") {
|
|
322
|
+
if (!sessionId) return false;
|
|
323
|
+
const ws = this.#sessionStreams.get(sessionId);
|
|
324
|
+
if (ws) {
|
|
325
|
+
try {
|
|
326
|
+
ws.close(1000, reason);
|
|
327
|
+
} catch {
|
|
328
|
+
/* best effort */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const wasTracked =
|
|
332
|
+
this.#sessionStreams.delete(sessionId) ||
|
|
333
|
+
this.#trackedSessions.delete(sessionId);
|
|
334
|
+
this.#trackedSessions.delete(sessionId);
|
|
335
|
+
this.#sessionReconnectCounts.delete(sessionId);
|
|
336
|
+
return wasTracked;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Close any session streams not in the allowed set.
|
|
341
|
+
* @param {Iterable<string>} allowedSessionIds
|
|
342
|
+
* @param {string} [reason="session no longer active"]
|
|
343
|
+
* @returns {number} Number of sessions pruned.
|
|
344
|
+
*/
|
|
345
|
+
pruneSessions(allowedSessionIds, reason = "session no longer active") {
|
|
346
|
+
const allowed = new Set(allowedSessionIds || []);
|
|
347
|
+
let pruned = 0;
|
|
348
|
+
for (const sessionId of Array.from(this.#trackedSessions)) {
|
|
349
|
+
if (!allowed.has(sessionId)) {
|
|
350
|
+
if (this.closeSession(sessionId, reason)) {
|
|
351
|
+
pruned += 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return pruned;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Private methods ────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Open the session-based execution-process stream WebSocket.
|
|
362
|
+
* Receives JSON Patch updates for all processes in the session.
|
|
363
|
+
*
|
|
364
|
+
* Initial snapshot: {"op":"replace","path":"/execution_processes","value":{...}}
|
|
365
|
+
* Live updates: {"op":"add|replace|remove","path":"/execution_processes/<id>","value":{...}}
|
|
366
|
+
*
|
|
367
|
+
* @param {string} sessionId
|
|
368
|
+
*/
|
|
369
|
+
#openSessionStream(sessionId) {
|
|
370
|
+
const shortSid = sessionId.slice(0, 8);
|
|
371
|
+
const params = new URLSearchParams({ session_id: sessionId });
|
|
372
|
+
const wsUrl = `${this.#wsBaseUrl}/api/execution-processes/stream/session/ws?${params}`;
|
|
373
|
+
|
|
374
|
+
let ws;
|
|
375
|
+
try {
|
|
376
|
+
ws = new WebSocket(wsUrl);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.warn(
|
|
379
|
+
`[vk-log-stream] failed to create session stream WS for ${shortSid}: ${err.message}`,
|
|
380
|
+
);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.#sessionStreams.set(sessionId, ws);
|
|
385
|
+
this.#sessionReconnectCounts.set(sessionId, 0);
|
|
386
|
+
|
|
387
|
+
ws.addEventListener("open", () => {
|
|
388
|
+
console.log(`[vk-log-stream] session stream connected (${shortSid})`);
|
|
389
|
+
this.#sessionReconnectCounts.set(sessionId, 0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
ws.addEventListener("message", (event) => {
|
|
393
|
+
this.#handleSessionStreamMessage(sessionId, event.data);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
ws.addEventListener("close", () => {
|
|
397
|
+
this.#sessionStreams.delete(sessionId);
|
|
398
|
+
if (!this.#running || !this.#trackedSessions.has(sessionId)) return;
|
|
399
|
+
|
|
400
|
+
const attempts = (this.#sessionReconnectCounts.get(sessionId) || 0) + 1;
|
|
401
|
+
this.#sessionReconnectCounts.set(sessionId, attempts);
|
|
402
|
+
|
|
403
|
+
if (attempts > MAX_SESSION_RECONNECT_ATTEMPTS) {
|
|
404
|
+
console.warn(
|
|
405
|
+
`[vk-log-stream] session stream ${shortSid} max reconnects (${MAX_SESSION_RECONNECT_ATTEMPTS}) reached`,
|
|
406
|
+
);
|
|
407
|
+
this.#trackedSessions.delete(sessionId);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const delay = Math.min(
|
|
412
|
+
SESSION_RECONNECT_DELAY_MS * Math.pow(1.5, attempts - 1),
|
|
413
|
+
60000,
|
|
414
|
+
);
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
if (this.#running && this.#trackedSessions.has(sessionId)) {
|
|
417
|
+
this.#openSessionStream(sessionId);
|
|
418
|
+
}
|
|
419
|
+
}, delay);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
ws.addEventListener("error", (event) => {
|
|
423
|
+
const msg = event?.message || event?.error?.message || "";
|
|
424
|
+
if (msg && !msg.includes("ECONNREFUSED")) {
|
|
425
|
+
console.warn(
|
|
426
|
+
`[vk-log-stream] session stream ${shortSid} error: ${msg}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Handle a message from the session execution-process stream.
|
|
434
|
+
*
|
|
435
|
+
* VK sends JSON Patch arrays. The initial snapshot replaces /execution_processes
|
|
436
|
+
* with an object keyed by process ID. Live updates add/replace/remove at
|
|
437
|
+
* /execution_processes/<processId>.
|
|
438
|
+
*
|
|
439
|
+
* @param {string} sessionId
|
|
440
|
+
* @param {string|Buffer} rawData
|
|
441
|
+
*/
|
|
442
|
+
#handleSessionStreamMessage(sessionId, rawData) {
|
|
443
|
+
const text = typeof rawData === "string" ? rawData : rawData.toString();
|
|
444
|
+
if (!text) return;
|
|
445
|
+
|
|
446
|
+
let msg;
|
|
447
|
+
try {
|
|
448
|
+
msg = JSON.parse(text);
|
|
449
|
+
} catch {
|
|
450
|
+
return; // ignore non-JSON
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── LogMsg::JsonPatch — the primary format for session streams ──
|
|
454
|
+
if (Array.isArray(msg.JsonPatch)) {
|
|
455
|
+
for (const patch of msg.JsonPatch) {
|
|
456
|
+
this.#processSessionPatch(sessionId, patch);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Raw JSON Patch array (some VK versions send this directly) ──
|
|
462
|
+
if (Array.isArray(msg)) {
|
|
463
|
+
for (const patch of msg) {
|
|
464
|
+
this.#processSessionPatch(sessionId, patch);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── LogMsg::Finished — session is done ──
|
|
470
|
+
if ("Finished" in msg || "finished" in msg) {
|
|
471
|
+
const shortSid = sessionId.slice(0, 8);
|
|
472
|
+
console.log(
|
|
473
|
+
`[vk-log-stream] session stream ${shortSid} received Finished`,
|
|
474
|
+
);
|
|
475
|
+
this.#trackedSessions.delete(sessionId);
|
|
476
|
+
const ws = this.#sessionStreams.get(sessionId);
|
|
477
|
+
if (ws) {
|
|
478
|
+
try {
|
|
479
|
+
ws.close(1000, "session finished");
|
|
480
|
+
} catch {
|
|
481
|
+
/* best effort */
|
|
482
|
+
}
|
|
483
|
+
this.#sessionStreams.delete(sessionId);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Process a single JSON Patch operation from the session stream.
|
|
491
|
+
* Extracts execution process IDs and connects to their raw-logs streams.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} sessionId
|
|
494
|
+
* @param {object} patch - { op, path, value }
|
|
495
|
+
*/
|
|
496
|
+
#processSessionPatch(sessionId, patch) {
|
|
497
|
+
if (!patch || !patch.path) return;
|
|
498
|
+
|
|
499
|
+
const { op, path, value } = patch;
|
|
500
|
+
|
|
501
|
+
// Initial snapshot: replace /execution_processes → object keyed by ID
|
|
502
|
+
if (
|
|
503
|
+
path === "/execution_processes" &&
|
|
504
|
+
op === "replace" &&
|
|
505
|
+
value &&
|
|
506
|
+
typeof value === "object"
|
|
507
|
+
) {
|
|
508
|
+
for (const [processId, proc] of Object.entries(value)) {
|
|
509
|
+
this.#maybeConnectProcess(processId, proc, sessionId);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Live update: add/replace /execution_processes/<processId>
|
|
515
|
+
const match = path.match(/^\/execution_processes\/([^/]+)$/);
|
|
516
|
+
if (match) {
|
|
517
|
+
const processId = match[1];
|
|
518
|
+
if (op === "remove") {
|
|
519
|
+
// Process removed — mark finished
|
|
520
|
+
this.#finished.add(processId);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (value && typeof value === "object") {
|
|
524
|
+
this.#maybeConnectProcess(processId, value, sessionId);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Connect to a process's raw-logs stream if it's running and not already tracked.
|
|
531
|
+
* @param {string} processId
|
|
532
|
+
* @param {object} proc - process data from VK (status, run_reason, etc.)
|
|
533
|
+
* @param {string} sessionId
|
|
534
|
+
*/
|
|
535
|
+
#maybeConnectProcess(processId, proc, sessionId) {
|
|
536
|
+
if (!processId) return;
|
|
537
|
+
|
|
538
|
+
const status = (proc.status || "").toLowerCase();
|
|
539
|
+
if (status === "completed" || status === "killed" || status === "failed") {
|
|
540
|
+
this.#finished.add(processId);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!this.#connections.has(processId) && !this.#finished.has(processId)) {
|
|
545
|
+
this.#knownProcessIds.add(processId);
|
|
546
|
+
const connMeta = {
|
|
547
|
+
sessionId,
|
|
548
|
+
runReason: proc.run_reason,
|
|
549
|
+
status,
|
|
550
|
+
};
|
|
551
|
+
this.#connectWebSocket(processId, connMeta);
|
|
552
|
+
|
|
553
|
+
// Notify the monitor so it can look up task metadata and call setProcessMeta()
|
|
554
|
+
if (this.#onProcessConnected) {
|
|
555
|
+
try {
|
|
556
|
+
this.#onProcessConnected(processId, connMeta);
|
|
557
|
+
} catch {
|
|
558
|
+
/* callback error — ignore */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Connect WebSocket to a specific execution process's raw-logs endpoint.
|
|
566
|
+
* @param {string} processId
|
|
567
|
+
* @param {object} meta
|
|
568
|
+
*/
|
|
569
|
+
#connectWebSocket(processId, meta = {}) {
|
|
570
|
+
const shortId = processId.slice(0, 8);
|
|
571
|
+
const wsUrl = `${this.#wsBaseUrl}/api/execution-processes/${processId}/raw-logs/ws`;
|
|
572
|
+
|
|
573
|
+
let ws;
|
|
574
|
+
try {
|
|
575
|
+
ws = new WebSocket(wsUrl);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
console.warn(
|
|
578
|
+
`[vk-log-stream] failed to create WebSocket for ${shortId}: ${err.message}`,
|
|
579
|
+
);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.#connections.set(processId, ws);
|
|
584
|
+
this.#reconnectCounts.set(processId, 0);
|
|
585
|
+
|
|
586
|
+
const logPrefix = `[vk-log-stream:${shortId}]`;
|
|
587
|
+
|
|
588
|
+
ws.addEventListener("open", () => {
|
|
589
|
+
console.log(
|
|
590
|
+
`${logPrefix} connected to raw-logs WebSocket` +
|
|
591
|
+
(meta.taskId ? ` (task: ${meta.taskId.slice(0, 8)})` : ""),
|
|
592
|
+
);
|
|
593
|
+
this.#reconnectCounts.set(processId, 0);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
ws.addEventListener("message", (event) => {
|
|
597
|
+
this.#handleMessage(processId, event.data, meta);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
ws.addEventListener("close", (event) => {
|
|
601
|
+
this.#connections.delete(processId);
|
|
602
|
+
if (this.#finished.has(processId)) {
|
|
603
|
+
console.log(`${logPrefix} WebSocket closed (process finished)`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (!this.#running) return;
|
|
607
|
+
|
|
608
|
+
const attempts = (this.#reconnectCounts.get(processId) || 0) + 1;
|
|
609
|
+
this.#reconnectCounts.set(processId, attempts);
|
|
610
|
+
|
|
611
|
+
if (attempts > MAX_RECONNECT_ATTEMPTS) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`${logPrefix} max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached, giving up`,
|
|
614
|
+
);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const delay = Math.min(
|
|
619
|
+
RECONNECT_DELAY_MS * Math.pow(1.5, attempts - 1),
|
|
620
|
+
30000,
|
|
621
|
+
);
|
|
622
|
+
console.log(
|
|
623
|
+
`${logPrefix} reconnecting in ${Math.round(delay)}ms (attempt ${attempts})`,
|
|
624
|
+
);
|
|
625
|
+
setTimeout(() => {
|
|
626
|
+
if (this.#running && !this.#finished.has(processId)) {
|
|
627
|
+
this.#connectWebSocket(processId, meta);
|
|
628
|
+
}
|
|
629
|
+
}, delay);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
ws.addEventListener("error", (event) => {
|
|
633
|
+
// Errors are followed by close events, so just log
|
|
634
|
+
const msg = event?.message || event?.error?.message || "unknown";
|
|
635
|
+
if (!msg.includes("ECONNREFUSED")) {
|
|
636
|
+
console.warn(`${logPrefix} WebSocket error: ${msg}`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Handle a WebSocket message from the raw-logs stream.
|
|
643
|
+
*
|
|
644
|
+
* VK sends LogMsg variants serialized as JSON:
|
|
645
|
+
* {"Stdout": "line"}
|
|
646
|
+
* {"Stderr": "line"}
|
|
647
|
+
* {"JsonPatch": [...]}
|
|
648
|
+
* {"Finished": ""}
|
|
649
|
+
*
|
|
650
|
+
* @param {string} processId
|
|
651
|
+
* @param {string|Buffer} rawData
|
|
652
|
+
* @param {object} meta
|
|
653
|
+
*/
|
|
654
|
+
#handleMessage(processId, rawData, meta) {
|
|
655
|
+
const text = typeof rawData === "string" ? rawData : rawData.toString();
|
|
656
|
+
if (!text) return;
|
|
657
|
+
|
|
658
|
+
let msg;
|
|
659
|
+
try {
|
|
660
|
+
msg = JSON.parse(text);
|
|
661
|
+
} catch {
|
|
662
|
+
// Not JSON — treat as raw text line
|
|
663
|
+
this.#emitLine(processId, text, "stdout", meta);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── LogMsg::Stdout ──
|
|
668
|
+
if (typeof msg.Stdout === "string") {
|
|
669
|
+
this.#emitLine(processId, msg.Stdout, "stdout", meta);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ── LogMsg::Stderr ──
|
|
674
|
+
if (typeof msg.Stderr === "string") {
|
|
675
|
+
this.#emitLine(processId, msg.Stderr, "stderr", meta);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ── LogMsg::Finished ──
|
|
680
|
+
if ("Finished" in msg) {
|
|
681
|
+
this.#finished.add(processId);
|
|
682
|
+
const shortId = processId.slice(0, 8);
|
|
683
|
+
console.log(`[vk-log-stream:${shortId}] execution process finished`);
|
|
684
|
+
const ws = this.#connections.get(processId);
|
|
685
|
+
if (ws) {
|
|
686
|
+
try {
|
|
687
|
+
ws.close(1000, "finished");
|
|
688
|
+
} catch {
|
|
689
|
+
/* best effort */
|
|
690
|
+
}
|
|
691
|
+
this.#connections.delete(processId);
|
|
692
|
+
}
|
|
693
|
+
// Write final marker to log files (raw + structured session log)
|
|
694
|
+
this.#writeToFile(
|
|
695
|
+
processId,
|
|
696
|
+
`\n--- [vk-log-stream] Process ${shortId} finished at ${new Date().toISOString()} ---\n`,
|
|
697
|
+
);
|
|
698
|
+
this.#writeSessionLogFooter(processId);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── LogMsg::JsonPatch — extract content from patch operations ──
|
|
703
|
+
if (Array.isArray(msg.JsonPatch)) {
|
|
704
|
+
for (const patch of msg.JsonPatch) {
|
|
705
|
+
if (patch?.value) {
|
|
706
|
+
const type = (patch.value.type || "").toUpperCase();
|
|
707
|
+
const content = patch.value.content || patch.value.text || "";
|
|
708
|
+
if (content) {
|
|
709
|
+
const stream =
|
|
710
|
+
type === "STDERR"
|
|
711
|
+
? "stderr"
|
|
712
|
+
: type === "STDOUT"
|
|
713
|
+
? "stdout"
|
|
714
|
+
: "stdout";
|
|
715
|
+
this.#emitLine(processId, content, stream, meta);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── LogMsg::SessionId, LogMsg::MessageId, LogMsg::Ready — informational ──
|
|
723
|
+
if (msg.SessionId || msg.MessageId || msg.Ready !== undefined) {
|
|
724
|
+
return; // Ignore informational messages
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ── LogMsg::finished (lowercase variant) — some VK versions send this ──
|
|
728
|
+
if (msg.finished === true || "finished" in msg) {
|
|
729
|
+
this.#finished.add(processId);
|
|
730
|
+
const shortId = processId.slice(0, 8);
|
|
731
|
+
console.log(`[vk-log-stream:${shortId}] execution process finished (lowercase variant)`);
|
|
732
|
+
const ws = this.#connections.get(processId);
|
|
733
|
+
if (ws) {
|
|
734
|
+
try {
|
|
735
|
+
ws.close(1000, "finished");
|
|
736
|
+
} catch {
|
|
737
|
+
/* best effort */
|
|
738
|
+
}
|
|
739
|
+
this.#connections.delete(processId);
|
|
740
|
+
}
|
|
741
|
+
this.#writeToFile(
|
|
742
|
+
processId,
|
|
743
|
+
`\n--- [vk-log-stream] Process ${shortId} finished at ${new Date().toISOString()} ---\n`,
|
|
744
|
+
);
|
|
745
|
+
this.#writeSessionLogFooter(processId);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Unknown format — log raw for debugging
|
|
750
|
+
const shortId = processId.slice(0, 8);
|
|
751
|
+
console.warn(
|
|
752
|
+
`[vk-log-stream:${shortId}] unknown message format: ${text.slice(0, 200)}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Emit a parsed log line to all outputs (file, console, session log, callback).
|
|
758
|
+
* @param {string} processId
|
|
759
|
+
* @param {string} content
|
|
760
|
+
* @param {"stdout"|"stderr"} stream
|
|
761
|
+
* @param {object} meta
|
|
762
|
+
*/
|
|
763
|
+
#emitLine(processId, content, stream, meta) {
|
|
764
|
+
// Strip trailing newlines since we add our own
|
|
765
|
+
const line = content.replace(/\r?\n$/, "");
|
|
766
|
+
if (!line) return;
|
|
767
|
+
|
|
768
|
+
const shortId = processId.slice(0, 8);
|
|
769
|
+
|
|
770
|
+
// ALWAYS invoke onLine callback BEFORE filtering so the anomaly detector
|
|
771
|
+
// sees every line — including errors wrapped in codex/event/ JSON envelopes
|
|
772
|
+
// that filterLine would otherwise drop.
|
|
773
|
+
if (this.#onLine) {
|
|
774
|
+
try {
|
|
775
|
+
this.#onLine(line, { processId, stream, ...meta });
|
|
776
|
+
} catch {
|
|
777
|
+
/* callback error — ignore */
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Apply noise filter (drop if false) — only affects file/console output
|
|
782
|
+
if (this.#filterLine && !this.#filterLine(line, { processId, stream })) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Write to per-process log file (raw)
|
|
787
|
+
this.#writeToFile(processId, `[${stream}] ${line}\n`);
|
|
788
|
+
|
|
789
|
+
// Write to structured session log (codex-exec style)
|
|
790
|
+
this.#writeSessionLog(processId, line, stream);
|
|
791
|
+
|
|
792
|
+
// Echo to console
|
|
793
|
+
if (this.#echo) {
|
|
794
|
+
const prefix = stream === "stderr" ? "ERR" : "OUT";
|
|
795
|
+
try {
|
|
796
|
+
process.stdout.write(`[vk:${shortId}:${prefix}] ${line}\n`);
|
|
797
|
+
} catch {
|
|
798
|
+
/* EPIPE */
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Write text to the per-process log file.
|
|
805
|
+
* @param {string} processId
|
|
806
|
+
* @param {string} text
|
|
807
|
+
*/
|
|
808
|
+
#writeToFile(processId, text) {
|
|
809
|
+
if (!this.#logDir) return;
|
|
810
|
+
const shortId = processId.slice(0, 8);
|
|
811
|
+
const logPath = resolve(this.#logDir, `vk-exec-${shortId}.log`);
|
|
812
|
+
try {
|
|
813
|
+
appendFileSync(logPath, text);
|
|
814
|
+
} catch {
|
|
815
|
+
/* best effort */
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Write a log line to the structured session log file (codex-exec style).
|
|
821
|
+
* On first call per process, writes a metadata header block.
|
|
822
|
+
*
|
|
823
|
+
* @param {string} processId
|
|
824
|
+
* @param {string} line - The log content (no prefix)
|
|
825
|
+
* @param {"stdout"|"stderr"} stream
|
|
826
|
+
*/
|
|
827
|
+
#writeSessionLog(processId, line, stream) {
|
|
828
|
+
if (!this.#sessionLogDir) return;
|
|
829
|
+
|
|
830
|
+
// Write header on first line
|
|
831
|
+
if (!this.#sessionLogHeaderWritten.has(processId)) {
|
|
832
|
+
this.#writeSessionLogHeader(processId);
|
|
833
|
+
this.#sessionLogHeaderWritten.add(processId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const logPath = this.getSessionLogPath(processId);
|
|
837
|
+
if (!logPath) return;
|
|
838
|
+
const prefix = stream === "stderr" ? "[stderr] " : "";
|
|
839
|
+
try {
|
|
840
|
+
appendFileSync(logPath, `${prefix}${line}\n`);
|
|
841
|
+
} catch {
|
|
842
|
+
/* best effort */
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Write the structured header to a session log file.
|
|
848
|
+
* Mirrors the codex-exec log format with VK-specific metadata.
|
|
849
|
+
*
|
|
850
|
+
* @param {string} processId
|
|
851
|
+
*/
|
|
852
|
+
#writeSessionLogHeader(processId) {
|
|
853
|
+
// getSessionLogPath lazily assigns _logStamp on first call
|
|
854
|
+
const logPath = this.getSessionLogPath(processId);
|
|
855
|
+
if (!logPath) return;
|
|
856
|
+
|
|
857
|
+
const meta = this.#processMeta.get(processId) || {};
|
|
858
|
+
const shortId = processId.slice(0, 8);
|
|
859
|
+
const now = new Date().toISOString();
|
|
860
|
+
|
|
861
|
+
const header = [
|
|
862
|
+
`# VK Session Execution Log`,
|
|
863
|
+
`# Timestamp: ${now}`,
|
|
864
|
+
`# Process ID: ${processId}`,
|
|
865
|
+
`# Process (short): ${shortId}`,
|
|
866
|
+
meta.attemptId
|
|
867
|
+
? `# Attempt ID: ${meta.attemptId}`
|
|
868
|
+
: `# Attempt ID: (unknown)`,
|
|
869
|
+
meta.taskId ? `# Task ID: ${meta.taskId}` : `# Task ID: (unknown)`,
|
|
870
|
+
meta.taskTitle
|
|
871
|
+
? `# Task Title: ${meta.taskTitle}`
|
|
872
|
+
: `# Task Title: (unknown)`,
|
|
873
|
+
meta.branch ? `# Branch: ${meta.branch}` : `# Branch: (unknown)`,
|
|
874
|
+
meta.sessionId
|
|
875
|
+
? `# Session ID: ${meta.sessionId}`
|
|
876
|
+
: `# Session ID: (unknown)`,
|
|
877
|
+
meta.executor
|
|
878
|
+
? `# Executor: ${meta.executor}${meta.executorVariant ? ` (${meta.executorVariant})` : ""}`
|
|
879
|
+
: `# Executor: (unknown)`,
|
|
880
|
+
`# VK Endpoint: ${this.#baseUrl}`,
|
|
881
|
+
``,
|
|
882
|
+
`## VK Agent Output Stream:`,
|
|
883
|
+
``,
|
|
884
|
+
].join("\n");
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
writeFileSync(logPath, header);
|
|
888
|
+
} catch {
|
|
889
|
+
/* best effort */
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Write a final footer to the structured session log when a process finishes.
|
|
895
|
+
* @param {string} processId
|
|
896
|
+
*/
|
|
897
|
+
#writeSessionLogFooter(processId) {
|
|
898
|
+
if (!this.#sessionLogDir) return;
|
|
899
|
+
if (!this.#sessionLogHeaderWritten.has(processId)) return; // no log started
|
|
900
|
+
|
|
901
|
+
const logPath = this.getSessionLogPath(processId);
|
|
902
|
+
if (!logPath) return;
|
|
903
|
+
const now = new Date().toISOString();
|
|
904
|
+
const shortId = processId.slice(0, 8);
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
appendFileSync(logPath, `\n\n## Process ${shortId} finished at ${now}\n`);
|
|
908
|
+
} catch {
|
|
909
|
+
/* best effort */
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export default VkLogStream;
|