@agentmeshhq/agent 0.4.15 → 0.4.18
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/dist/__tests__/bootstrap.test.js +24 -0
- package/dist/__tests__/bootstrap.test.js.map +1 -1
- package/dist/__tests__/injection-verify.test.d.ts +1 -0
- package/dist/__tests__/injection-verify.test.js +93 -0
- package/dist/__tests__/injection-verify.test.js.map +1 -0
- package/dist/__tests__/injector.test.js +115 -1
- package/dist/__tests__/injector.test.js.map +1 -1
- package/dist/__tests__/lead-loop.test.d.ts +1 -0
- package/dist/__tests__/lead-loop.test.js +170 -0
- package/dist/__tests__/lead-loop.test.js.map +1 -0
- package/dist/__tests__/relay.test.d.ts +1 -0
- package/dist/__tests__/relay.test.js +17 -0
- package/dist/__tests__/relay.test.js.map +1 -0
- package/dist/__tests__/roles.test.d.ts +1 -0
- package/dist/__tests__/roles.test.js +78 -0
- package/dist/__tests__/roles.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +17 -0
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/session-recovery.test.js +214 -11
- package/dist/__tests__/session-recovery.test.js.map +1 -1
- package/dist/__tests__/start-team-id.test.js +8 -3
- package/dist/__tests__/start-team-id.test.js.map +1 -1
- package/dist/__tests__/startup-diagnostics.test.d.ts +1 -0
- package/dist/__tests__/startup-diagnostics.test.js +250 -0
- package/dist/__tests__/startup-diagnostics.test.js.map +1 -0
- package/dist/__tests__/tmux-runtime.test.js +13 -0
- package/dist/__tests__/tmux-runtime.test.js.map +1 -1
- package/dist/__tests__/watcher-queue.test.d.ts +1 -0
- package/dist/__tests__/watcher-queue.test.js +85 -0
- package/dist/__tests__/watcher-queue.test.js.map +1 -0
- package/dist/__tests__/watcher-state.test.d.ts +1 -0
- package/dist/__tests__/watcher-state.test.js +159 -0
- package/dist/__tests__/watcher-state.test.js.map +1 -0
- package/dist/cli/commands.d.ts +32 -0
- package/dist/cli/commands.js +165 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.js +95 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/relay.d.ts +1 -0
- package/dist/cli/relay.js +16 -2
- package/dist/cli/relay.js.map +1 -1
- package/dist/cli/start.d.ts +2 -0
- package/dist/cli/start.js +42 -7
- package/dist/cli/start.js.map +1 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/core/chat-output-parser.d.ts +24 -0
- package/dist/core/chat-output-parser.js +134 -0
- package/dist/core/chat-output-parser.js.map +1 -0
- package/dist/core/chat-output-parser.test.d.ts +7 -0
- package/dist/core/chat-output-parser.test.js +130 -0
- package/dist/core/chat-output-parser.test.js.map +1 -0
- package/dist/core/daemon/bootstrap.js +14 -4
- package/dist/core/daemon/bootstrap.js.map +1 -1
- package/dist/core/daemon/context-template.d.ts +5 -1
- package/dist/core/daemon/context-template.js +16 -2
- package/dist/core/daemon/context-template.js.map +1 -1
- package/dist/core/daemon/crash-log.js +5 -0
- package/dist/core/daemon/crash-log.js.map +1 -1
- package/dist/core/daemon/injection-verify.d.ts +25 -0
- package/dist/core/daemon/injection-verify.js +94 -0
- package/dist/core/daemon/injection-verify.js.map +1 -0
- package/dist/core/daemon/lead-loop.d.ts +22 -0
- package/dist/core/daemon/lead-loop.js +155 -0
- package/dist/core/daemon/lead-loop.js.map +1 -0
- package/dist/core/daemon/roles.d.ts +25 -0
- package/dist/core/daemon/roles.js +46 -0
- package/dist/core/daemon/roles.js.map +1 -0
- package/dist/core/daemon/session-recovery.d.ts +18 -1
- package/dist/core/daemon/session-recovery.js +89 -5
- package/dist/core/daemon/session-recovery.js.map +1 -1
- package/dist/core/daemon/startup-diagnostics.d.ts +76 -0
- package/dist/core/daemon/startup-diagnostics.js +277 -0
- package/dist/core/daemon/startup-diagnostics.js.map +1 -0
- package/dist/core/daemon/watcher-loop.d.ts +27 -0
- package/dist/core/daemon/watcher-loop.js +134 -0
- package/dist/core/daemon/watcher-loop.js.map +1 -0
- package/dist/core/daemon/watcher-queue.d.ts +33 -0
- package/dist/core/daemon/watcher-queue.js +71 -0
- package/dist/core/daemon/watcher-queue.js.map +1 -0
- package/dist/core/daemon/watcher-state.d.ts +66 -0
- package/dist/core/daemon/watcher-state.js +151 -0
- package/dist/core/daemon/watcher-state.js.map +1 -0
- package/dist/core/daemon.d.ts +14 -0
- package/dist/core/daemon.js +182 -3
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/injector.js +198 -1
- package/dist/core/injector.js.map +1 -1
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +19 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner/build.js +7 -31
- package/dist/core/runner/build.js.map +1 -1
- package/dist/core/runner/detect.js +2 -8
- package/dist/core/runner/detect.js.map +1 -1
- package/dist/core/runner/index.d.ts +3 -1
- package/dist/core/runner/index.js +2 -0
- package/dist/core/runner/index.js.map +1 -1
- package/dist/core/runner/kimi-models.d.ts +4 -0
- package/dist/core/runner/kimi-models.js +24 -0
- package/dist/core/runner/kimi-models.js.map +1 -0
- package/dist/core/runner/registry.d.ts +3 -0
- package/dist/core/runner/registry.js +75 -0
- package/dist/core/runner/registry.js.map +1 -0
- package/dist/core/runner/types.d.ts +17 -1
- package/dist/core/tmux-runtime.d.ts +1 -1
- package/dist/core/tmux-runtime.js +13 -0
- package/dist/core/tmux-runtime.js.map +1 -1
- package/dist/core/tmux.d.ts +4 -0
- package/dist/core/tmux.js +49 -11
- package/dist/core/tmux.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup Diagnostics for Agent Tmux Sessions
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging for runner startup to diagnose
|
|
5
|
+
* session creation failures and early exits.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
const DIAGNOSTICS_DIR = path.join(os.homedir(), ".agentmesh", "diagnostics");
|
|
12
|
+
const STARTUP_WINDOW_MS = 15000; // 15 second window for startup monitoring
|
|
13
|
+
/**
|
|
14
|
+
* Ensures the diagnostics directory exists
|
|
15
|
+
*/
|
|
16
|
+
function ensureDiagnosticsDir() {
|
|
17
|
+
if (!fs.existsSync(DIAGNOSTICS_DIR)) {
|
|
18
|
+
fs.mkdirSync(DIAGNOSTICS_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the path to the diagnostics log file for an agent
|
|
23
|
+
*/
|
|
24
|
+
function getDiagnosticsPath(agentName) {
|
|
25
|
+
return path.join(DIAGNOSTICS_DIR, `${agentName}-startup.jsonl`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Redacts sensitive environment variable values, keeping only keys
|
|
29
|
+
*/
|
|
30
|
+
export function redactEnvKeys(env) {
|
|
31
|
+
const sensitiveKeys = new Set([
|
|
32
|
+
"token",
|
|
33
|
+
"key",
|
|
34
|
+
"secret",
|
|
35
|
+
"password",
|
|
36
|
+
"auth",
|
|
37
|
+
"credential",
|
|
38
|
+
"api_key",
|
|
39
|
+
"apikey",
|
|
40
|
+
]);
|
|
41
|
+
return Object.entries(env)
|
|
42
|
+
.filter(([, value]) => value !== undefined && value !== "")
|
|
43
|
+
.map(([key]) => {
|
|
44
|
+
const lowerKey = key.toLowerCase();
|
|
45
|
+
// Mark sensitive keys with [REDACTED] suffix
|
|
46
|
+
const isSensitive = sensitiveKeys.has(lowerKey) || [...sensitiveKeys].some((sk) => lowerKey.includes(sk));
|
|
47
|
+
return isSensitive ? `${key}=[REDACTED]` : key;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detects the runner type from the command
|
|
52
|
+
*/
|
|
53
|
+
export function detectRunnerKind(command) {
|
|
54
|
+
const cmd = command.trim().toLowerCase();
|
|
55
|
+
if (cmd.startsWith("opencode"))
|
|
56
|
+
return "opencode";
|
|
57
|
+
if (cmd.startsWith("claude"))
|
|
58
|
+
return "claude";
|
|
59
|
+
if (cmd.startsWith("codex"))
|
|
60
|
+
return "codex";
|
|
61
|
+
return "other";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Logs startup diagnostics for a new tmux session
|
|
65
|
+
*/
|
|
66
|
+
export function logSessionStartup(agentName, diagnostics) {
|
|
67
|
+
ensureDiagnosticsDir();
|
|
68
|
+
const entry = {
|
|
69
|
+
type: "startup",
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
agentName,
|
|
72
|
+
data: diagnostics,
|
|
73
|
+
};
|
|
74
|
+
const logPath = getDiagnosticsPath(agentName);
|
|
75
|
+
fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
|
|
76
|
+
// Also log to console for immediate visibility
|
|
77
|
+
console.log(`[STARTUP] ${agentName}: ${diagnostics.resolvedCommand}`);
|
|
78
|
+
console.log(`[STARTUP] Runner: ${diagnostics.runnerKind}, Autonomous: ${diagnostics.autonomous ?? false}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Logs a process exit event
|
|
82
|
+
*/
|
|
83
|
+
export function logProcessExit(agentName, exitEvent) {
|
|
84
|
+
ensureDiagnosticsDir();
|
|
85
|
+
const entry = {
|
|
86
|
+
type: "exit",
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
agentName,
|
|
89
|
+
data: exitEvent,
|
|
90
|
+
};
|
|
91
|
+
const logPath = getDiagnosticsPath(agentName);
|
|
92
|
+
fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
|
|
93
|
+
// Log with appropriate severity
|
|
94
|
+
const severity = exitEvent.elapsedMs < STARTUP_WINDOW_MS ? "ERROR" : "WARN";
|
|
95
|
+
console.log(`[${severity}] ${agentName}: Process exited (code=${exitEvent.code}, signal=${exitEvent.signal}, elapsed=${exitEvent.elapsedMs}ms) - ${exitEvent.reason}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads the recent diagnostics for an agent
|
|
99
|
+
*/
|
|
100
|
+
export function readStartupDiagnostics(agentName) {
|
|
101
|
+
try {
|
|
102
|
+
const logPath = getDiagnosticsPath(agentName);
|
|
103
|
+
if (!fs.existsSync(logPath)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
107
|
+
return content
|
|
108
|
+
.trim()
|
|
109
|
+
.split("\n")
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map((line) => JSON.parse(line));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Clears old diagnostics for an agent (keeps last 100 entries)
|
|
119
|
+
*/
|
|
120
|
+
export function cleanupOldDiagnostics(agentName) {
|
|
121
|
+
try {
|
|
122
|
+
const entries = readStartupDiagnostics(agentName);
|
|
123
|
+
if (entries.length <= 100)
|
|
124
|
+
return;
|
|
125
|
+
const logPath = getDiagnosticsPath(agentName);
|
|
126
|
+
const recent = entries.slice(-100);
|
|
127
|
+
fs.writeFileSync(logPath, `${recent.map((e) => JSON.stringify(e)).join("\n")}\n`);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Non-fatal: cleanup failure should not break anything
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Captures the current pane PID for an agent's session
|
|
135
|
+
*/
|
|
136
|
+
export function capturePanePid(sessionName) {
|
|
137
|
+
try {
|
|
138
|
+
const output = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_pid}"`, {
|
|
139
|
+
encoding: "utf-8",
|
|
140
|
+
timeout: 5000,
|
|
141
|
+
}).trim();
|
|
142
|
+
const pid = parseInt(output, 10);
|
|
143
|
+
return Number.isNaN(pid) ? null : pid;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Monitors a tmux session for early process exit
|
|
151
|
+
* Returns a cleanup function to stop monitoring
|
|
152
|
+
*/
|
|
153
|
+
export function monitorSessionExit(agentName, sessionName, startTime, onEarlyExit) {
|
|
154
|
+
const initialPid = capturePanePid(sessionName);
|
|
155
|
+
let checkInterval = null;
|
|
156
|
+
let hasExited = false;
|
|
157
|
+
// Stop monitoring after startup window + grace period
|
|
158
|
+
const stopMonitoringAt = startTime.getTime() + STARTUP_WINDOW_MS + 5000;
|
|
159
|
+
checkInterval = setInterval(() => {
|
|
160
|
+
if (hasExited)
|
|
161
|
+
return;
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
if (now > stopMonitoringAt) {
|
|
164
|
+
// Startup window passed, stop monitoring
|
|
165
|
+
if (checkInterval) {
|
|
166
|
+
clearInterval(checkInterval);
|
|
167
|
+
checkInterval = null;
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
// Check if session still exists
|
|
173
|
+
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, { timeout: 1000 });
|
|
174
|
+
// Check if pane is dead
|
|
175
|
+
const paneInfo = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_pid}:#{pane_dead}" 2>/dev/null`, { encoding: "utf-8", timeout: 1000 }).trim();
|
|
176
|
+
const [currentPidStr, dead] = paneInfo.split(":");
|
|
177
|
+
const currentPid = parseInt(currentPidStr, 10);
|
|
178
|
+
if (dead === "1") {
|
|
179
|
+
hasExited = true;
|
|
180
|
+
const elapsedMs = now - startTime.getTime();
|
|
181
|
+
const event = {
|
|
182
|
+
code: null,
|
|
183
|
+
signal: null,
|
|
184
|
+
elapsedMs,
|
|
185
|
+
reason: "pane_died",
|
|
186
|
+
};
|
|
187
|
+
logProcessExit(agentName, event);
|
|
188
|
+
onEarlyExit?.(event);
|
|
189
|
+
if (checkInterval) {
|
|
190
|
+
clearInterval(checkInterval);
|
|
191
|
+
checkInterval = null;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Check if PID changed (process restarted/exited)
|
|
196
|
+
if (initialPid && !Number.isNaN(currentPid) && currentPid !== initialPid && initialPid > 0) {
|
|
197
|
+
hasExited = true;
|
|
198
|
+
const elapsedMs = now - startTime.getTime();
|
|
199
|
+
const event = {
|
|
200
|
+
code: null,
|
|
201
|
+
signal: null,
|
|
202
|
+
elapsedMs,
|
|
203
|
+
reason: "process_replaced",
|
|
204
|
+
};
|
|
205
|
+
logProcessExit(agentName, event);
|
|
206
|
+
onEarlyExit?.(event);
|
|
207
|
+
if (checkInterval) {
|
|
208
|
+
clearInterval(checkInterval);
|
|
209
|
+
checkInterval = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Session no longer exists - process exited
|
|
215
|
+
if (!hasExited) {
|
|
216
|
+
hasExited = true;
|
|
217
|
+
const elapsedMs = now - startTime.getTime();
|
|
218
|
+
const event = {
|
|
219
|
+
code: null,
|
|
220
|
+
signal: null,
|
|
221
|
+
elapsedMs,
|
|
222
|
+
reason: "session_terminated",
|
|
223
|
+
};
|
|
224
|
+
logProcessExit(agentName, event);
|
|
225
|
+
onEarlyExit?.(event);
|
|
226
|
+
if (checkInterval) {
|
|
227
|
+
clearInterval(checkInterval);
|
|
228
|
+
checkInterval = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, 1000); // Check every second
|
|
233
|
+
// Return cleanup function
|
|
234
|
+
return () => {
|
|
235
|
+
if (checkInterval) {
|
|
236
|
+
clearInterval(checkInterval);
|
|
237
|
+
checkInterval = null;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Formats a diagnostic summary for display
|
|
243
|
+
*/
|
|
244
|
+
export function formatDiagnosticSummary(agentName) {
|
|
245
|
+
const entries = readStartupDiagnostics(agentName);
|
|
246
|
+
if (entries.length === 0) {
|
|
247
|
+
return `No startup diagnostics found for ${agentName}`;
|
|
248
|
+
}
|
|
249
|
+
const recent = entries.slice(-5); // Last 5 entries
|
|
250
|
+
const lines = [
|
|
251
|
+
`\n=== Startup Diagnostics for ${agentName} ===`,
|
|
252
|
+
`Total entries: ${entries.length}`,
|
|
253
|
+
"",
|
|
254
|
+
];
|
|
255
|
+
for (const entry of recent) {
|
|
256
|
+
if (entry.type === "startup") {
|
|
257
|
+
const data = entry.data;
|
|
258
|
+
lines.push(`[${entry.timestamp}] STARTUP`);
|
|
259
|
+
lines.push(` Command: ${data.resolvedCommand}`);
|
|
260
|
+
lines.push(` Runner: ${data.runnerKind}`);
|
|
261
|
+
lines.push(` Session: ${data.sessionName}`);
|
|
262
|
+
lines.push(` Workdir: ${data.workdir ?? "(not set)"}`);
|
|
263
|
+
lines.push(` Autonomous: ${data.autonomous ?? false}`);
|
|
264
|
+
lines.push(` Env vars: ${data.envKeys.join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const data = entry.data;
|
|
268
|
+
lines.push(`[${entry.timestamp}] EXIT`);
|
|
269
|
+
lines.push(` Code: ${data.code ?? "N/A"}, Signal: ${data.signal ?? "N/A"}`);
|
|
270
|
+
lines.push(` Elapsed: ${data.elapsedMs}ms`);
|
|
271
|
+
lines.push(` Reason: ${data.reason}`);
|
|
272
|
+
}
|
|
273
|
+
lines.push("");
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=startup-diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-diagnostics.js","sourceRoot":"","sources":["../../../src/core/daemon/startup-diagnostics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered watcher loop.
|
|
3
|
+
*
|
|
4
|
+
* Two event sources feed the tick queue:
|
|
5
|
+
* 1. Scheduled ticker — every 2 min, full state fetch
|
|
6
|
+
* 2. WebSocket events — immediate reactive ticks (coalesced while LLM is busy)
|
|
7
|
+
*
|
|
8
|
+
* A consumer loop polls every 5s:
|
|
9
|
+
* - If LLM is processing → check tmux for completion
|
|
10
|
+
* - If idle and queue has items → dequeue, fetch state, inject into tmux
|
|
11
|
+
*
|
|
12
|
+
* Epic #883
|
|
13
|
+
*/
|
|
14
|
+
import type { WatcherTickQueue } from "./watcher-queue.js";
|
|
15
|
+
import { type WatcherContext } from "./watcher-state.js";
|
|
16
|
+
export interface WatcherLoopHandle {
|
|
17
|
+
stop: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function startWatcherLoop(ctx: WatcherContext, queue: WatcherTickQueue): WatcherLoopHandle;
|
|
20
|
+
/**
|
|
21
|
+
* Intercept WebSocket events relevant to the watcher and enqueue reactive ticks.
|
|
22
|
+
* Returns true if the event was consumed (watcher-relevant), false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleWatcherWebSocketEvent(event: {
|
|
25
|
+
type: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}, queue: WatcherTickQueue): boolean;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered watcher loop.
|
|
3
|
+
*
|
|
4
|
+
* Two event sources feed the tick queue:
|
|
5
|
+
* 1. Scheduled ticker — every 2 min, full state fetch
|
|
6
|
+
* 2. WebSocket events — immediate reactive ticks (coalesced while LLM is busy)
|
|
7
|
+
*
|
|
8
|
+
* A consumer loop polls every 5s:
|
|
9
|
+
* - If LLM is processing → check tmux for completion
|
|
10
|
+
* - If idle and queue has items → dequeue, fetch state, inject into tmux
|
|
11
|
+
*
|
|
12
|
+
* Epic #883
|
|
13
|
+
*/
|
|
14
|
+
import { captureSessionOutput, sendKeys } from "../tmux.js";
|
|
15
|
+
import { fetchWatcherState, formatStateForLlm } from "./watcher-state.js";
|
|
16
|
+
const TICK_INTERVAL_MS = 120_000; // 2 minutes
|
|
17
|
+
const CONSUMER_POLL_MS = 5_000; // 5 seconds
|
|
18
|
+
const IDLE_THRESHOLD_MS = 10_000; // 10s no output change → tick complete
|
|
19
|
+
const HARD_TIMEOUT_MS = 180_000; // 3 min max per tick
|
|
20
|
+
/**
|
|
21
|
+
* Detect whether the LLM has finished processing by checking tmux output
|
|
22
|
+
* stability. Returns true if the output hasn't changed for IDLE_THRESHOLD_MS.
|
|
23
|
+
*/
|
|
24
|
+
function detectTickComplete(agentName, lastOutputSnapshot, lastOutputChangeAt) {
|
|
25
|
+
const current = captureSessionOutput(agentName, 30);
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
if (current !== lastOutputSnapshot) {
|
|
28
|
+
// Output changed — LLM is still working
|
|
29
|
+
return { complete: false, currentOutput: current };
|
|
30
|
+
}
|
|
31
|
+
// Output stable — check if long enough
|
|
32
|
+
if (now - lastOutputChangeAt >= IDLE_THRESHOLD_MS) {
|
|
33
|
+
return { complete: true, currentOutput: current };
|
|
34
|
+
}
|
|
35
|
+
return { complete: false, currentOutput: current };
|
|
36
|
+
}
|
|
37
|
+
export function startWatcherLoop(ctx, queue) {
|
|
38
|
+
let lastInjectionAt = 0;
|
|
39
|
+
let lastOutputSnapshot = null;
|
|
40
|
+
let lastOutputChangeAt = 0;
|
|
41
|
+
let stopped = false;
|
|
42
|
+
// --- Scheduled ticker: enqueue a scheduled tick every 2 min ---
|
|
43
|
+
const scheduledTimer = setInterval(() => {
|
|
44
|
+
if (stopped)
|
|
45
|
+
return;
|
|
46
|
+
queue.enqueue("scheduled", ["interval"]);
|
|
47
|
+
ctx.log("[watcher-loop] Scheduled tick enqueued");
|
|
48
|
+
}, TICK_INTERVAL_MS);
|
|
49
|
+
// Enqueue the first tick immediately
|
|
50
|
+
queue.enqueue("scheduled", ["startup"]);
|
|
51
|
+
// --- Consumer loop: process one tick at a time ---
|
|
52
|
+
const consumerTimer = setInterval(async () => {
|
|
53
|
+
if (stopped)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
if (queue.isProcessing()) {
|
|
57
|
+
// Check if LLM finished processing
|
|
58
|
+
const elapsed = Date.now() - lastInjectionAt;
|
|
59
|
+
if (elapsed >= HARD_TIMEOUT_MS) {
|
|
60
|
+
ctx.log(`[watcher-loop] Hard timeout (${Math.round(elapsed / 1000)}s) — forcing tick complete`);
|
|
61
|
+
queue.markComplete();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { complete, currentOutput } = detectTickComplete(ctx.agentName, lastOutputSnapshot, lastOutputChangeAt);
|
|
65
|
+
if (currentOutput !== lastOutputSnapshot) {
|
|
66
|
+
lastOutputSnapshot = currentOutput;
|
|
67
|
+
lastOutputChangeAt = Date.now();
|
|
68
|
+
}
|
|
69
|
+
if (complete) {
|
|
70
|
+
ctx.log("[watcher-loop] Tick complete (output idle)");
|
|
71
|
+
queue.markComplete();
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Not processing — check if there's a tick to dequeue
|
|
76
|
+
if (queue.size() === 0)
|
|
77
|
+
return;
|
|
78
|
+
const tick = queue.dequeue();
|
|
79
|
+
if (!tick)
|
|
80
|
+
return;
|
|
81
|
+
const tickNumber = queue.nextTickNumber();
|
|
82
|
+
ctx.log(`[watcher-loop] Processing tick #${tickNumber} (${tick.source}: ${tick.triggeredBy.join(", ")})`);
|
|
83
|
+
// Fetch fresh state from hub
|
|
84
|
+
const state = await fetchWatcherState(ctx);
|
|
85
|
+
// Format and inject into tmux
|
|
86
|
+
const message = formatStateForLlm(state, tickNumber, tick.triggeredBy);
|
|
87
|
+
const sent = sendKeys(ctx.agentName, message);
|
|
88
|
+
if (!sent) {
|
|
89
|
+
ctx.log("[watcher-loop] Failed to inject tick into tmux — marking complete");
|
|
90
|
+
queue.markComplete();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Track for completion detection
|
|
94
|
+
lastInjectionAt = Date.now();
|
|
95
|
+
lastOutputSnapshot = captureSessionOutput(ctx.agentName, 30);
|
|
96
|
+
lastOutputChangeAt = Date.now();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
ctx.log(`[watcher-loop] Consumer error: ${err.message}`);
|
|
100
|
+
// If processing, mark complete to avoid stuck state
|
|
101
|
+
if (queue.isProcessing()) {
|
|
102
|
+
queue.markComplete();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, CONSUMER_POLL_MS);
|
|
106
|
+
return {
|
|
107
|
+
stop() {
|
|
108
|
+
stopped = true;
|
|
109
|
+
clearInterval(scheduledTimer);
|
|
110
|
+
clearInterval(consumerTimer);
|
|
111
|
+
ctx.log("[watcher-loop] Stopped");
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Intercept WebSocket events relevant to the watcher and enqueue reactive ticks.
|
|
117
|
+
* Returns true if the event was consumed (watcher-relevant), false otherwise.
|
|
118
|
+
*/
|
|
119
|
+
export function handleWatcherWebSocketEvent(event, queue) {
|
|
120
|
+
const watcherEvents = new Set([
|
|
121
|
+
"handoff.received",
|
|
122
|
+
"handoff.created",
|
|
123
|
+
"handoffs.updated",
|
|
124
|
+
"claims.updated",
|
|
125
|
+
"blockers.updated",
|
|
126
|
+
"blocker.resolved",
|
|
127
|
+
]);
|
|
128
|
+
if (watcherEvents.has(event.type)) {
|
|
129
|
+
queue.enqueue("reactive", [event.type]);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=watcher-loop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher-loop.js","sourceRoot":"","sources":["../../../src/core/daemon/watcher-loop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory tick queue for the LLM-powered watcher.
|
|
3
|
+
*
|
|
4
|
+
* Coalesces rapid WebSocket events into a single reactive tick and replaces
|
|
5
|
+
* stale scheduled ticks so the LLM always gets the freshest state.
|
|
6
|
+
*
|
|
7
|
+
* Epic #883
|
|
8
|
+
*/
|
|
9
|
+
export type TickSource = "scheduled" | "reactive";
|
|
10
|
+
export interface TickItem {
|
|
11
|
+
source: TickSource;
|
|
12
|
+
triggeredBy: string[];
|
|
13
|
+
queuedAt: Date;
|
|
14
|
+
}
|
|
15
|
+
export declare class WatcherTickQueue {
|
|
16
|
+
private queue;
|
|
17
|
+
private processing;
|
|
18
|
+
private tickCounter;
|
|
19
|
+
/**
|
|
20
|
+
* Enqueue a tick with deduplication:
|
|
21
|
+
* - Reactive + reactive → coalesce (merge triggeredBy, keep latest queuedAt)
|
|
22
|
+
* - Scheduled + scheduled → replace (only latest snapshot matters)
|
|
23
|
+
* - Otherwise → push new item
|
|
24
|
+
*/
|
|
25
|
+
enqueue(source: TickSource, triggeredBy: string[]): void;
|
|
26
|
+
dequeue(): TickItem | null;
|
|
27
|
+
markComplete(): void;
|
|
28
|
+
isProcessing(): boolean;
|
|
29
|
+
size(): number;
|
|
30
|
+
nextTickNumber(): number;
|
|
31
|
+
/** Reset for testing */
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory tick queue for the LLM-powered watcher.
|
|
3
|
+
*
|
|
4
|
+
* Coalesces rapid WebSocket events into a single reactive tick and replaces
|
|
5
|
+
* stale scheduled ticks so the LLM always gets the freshest state.
|
|
6
|
+
*
|
|
7
|
+
* Epic #883
|
|
8
|
+
*/
|
|
9
|
+
const MAX_QUEUE_SIZE = 10;
|
|
10
|
+
export class WatcherTickQueue {
|
|
11
|
+
queue = [];
|
|
12
|
+
processing = false;
|
|
13
|
+
tickCounter = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Enqueue a tick with deduplication:
|
|
16
|
+
* - Reactive + reactive → coalesce (merge triggeredBy, keep latest queuedAt)
|
|
17
|
+
* - Scheduled + scheduled → replace (only latest snapshot matters)
|
|
18
|
+
* - Otherwise → push new item
|
|
19
|
+
*/
|
|
20
|
+
enqueue(source, triggeredBy) {
|
|
21
|
+
const last = this.queue.at(-1);
|
|
22
|
+
if (last && source === "reactive" && last.source === "reactive") {
|
|
23
|
+
// Coalesce: merge trigger reasons, update timestamp
|
|
24
|
+
for (const t of triggeredBy) {
|
|
25
|
+
if (!last.triggeredBy.includes(t)) {
|
|
26
|
+
last.triggeredBy.push(t);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
last.queuedAt = new Date();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (last && source === "scheduled" && last.source === "scheduled") {
|
|
33
|
+
// Replace stale scheduled tick
|
|
34
|
+
last.triggeredBy = triggeredBy;
|
|
35
|
+
last.queuedAt = new Date();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
39
|
+
// Drop oldest to prevent unbounded growth
|
|
40
|
+
this.queue.shift();
|
|
41
|
+
}
|
|
42
|
+
this.queue.push({ source, triggeredBy, queuedAt: new Date() });
|
|
43
|
+
}
|
|
44
|
+
dequeue() {
|
|
45
|
+
const item = this.queue.shift() ?? null;
|
|
46
|
+
if (item) {
|
|
47
|
+
this.processing = true;
|
|
48
|
+
}
|
|
49
|
+
return item;
|
|
50
|
+
}
|
|
51
|
+
markComplete() {
|
|
52
|
+
this.processing = false;
|
|
53
|
+
}
|
|
54
|
+
isProcessing() {
|
|
55
|
+
return this.processing;
|
|
56
|
+
}
|
|
57
|
+
size() {
|
|
58
|
+
return this.queue.length;
|
|
59
|
+
}
|
|
60
|
+
nextTickNumber() {
|
|
61
|
+
this.tickCounter += 1;
|
|
62
|
+
return this.tickCounter;
|
|
63
|
+
}
|
|
64
|
+
/** Reset for testing */
|
|
65
|
+
clear() {
|
|
66
|
+
this.queue = [];
|
|
67
|
+
this.processing = false;
|
|
68
|
+
this.tickCounter = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=watcher-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher-queue.js","sourceRoot":"","sources":["../../../src/core/daemon/watcher-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,MAAM,OAAO,gBAAgB;IACnB,KAAK,GAAe,EAAE,CAAC;IACvB,UAAU,GAAG,KAAK,CAAC;IACnB,WAAW,GAAG,CAAC,CAAC;IAExB;;;;;OAKG;IACH,OAAO,CAAC,MAAkB,EAAE,WAAqB;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE/B,IAAI,IAAI,IAAI,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAChE,oDAAoD;YACpD,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;gBAC5B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;oBAClC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC3B,CAAC;YACH,CAAC;YACD,IAAI,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,IAAI,IAAI,IAAI,MAAM,KAAK,WAAW,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAClE,+BAA+B;YAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;YAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,cAAc,EAAE,CAAC;YACxC,0CAA0C;YAC1C,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC;QACxC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,YAAY;QACV,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,cAAc;QACZ,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches project state from hub APIs and formats it for LLM injection.
|
|
3
|
+
*
|
|
4
|
+
* Each tick sends a full snapshot — the LLM reasons from a single tick
|
|
5
|
+
* without needing conversation history.
|
|
6
|
+
*
|
|
7
|
+
* Epic #883
|
|
8
|
+
*/
|
|
9
|
+
export interface WatcherContext {
|
|
10
|
+
hubUrl: string;
|
|
11
|
+
token: string;
|
|
12
|
+
workspace: string;
|
|
13
|
+
teamId: string;
|
|
14
|
+
agentName: string;
|
|
15
|
+
log: (msg: string) => void;
|
|
16
|
+
}
|
|
17
|
+
interface HandoffSnapshot {
|
|
18
|
+
handoff_id: string;
|
|
19
|
+
from_agent_id: string | null;
|
|
20
|
+
to_agent_id: string | null;
|
|
21
|
+
scope: string;
|
|
22
|
+
status: string;
|
|
23
|
+
created_at: string;
|
|
24
|
+
accepted_at: string | null;
|
|
25
|
+
}
|
|
26
|
+
interface ClaimSnapshot {
|
|
27
|
+
claim_id: string;
|
|
28
|
+
agent_id: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
paths: string[];
|
|
31
|
+
status: string;
|
|
32
|
+
expires_at: string;
|
|
33
|
+
}
|
|
34
|
+
interface BlockerSnapshot {
|
|
35
|
+
blocker_id: string;
|
|
36
|
+
agent_id: string;
|
|
37
|
+
scope: string;
|
|
38
|
+
category: string;
|
|
39
|
+
description: string;
|
|
40
|
+
status: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
}
|
|
43
|
+
interface AgentSnapshot {
|
|
44
|
+
agent_id: string;
|
|
45
|
+
display_name: string;
|
|
46
|
+
status: string;
|
|
47
|
+
model: string;
|
|
48
|
+
last_heartbeat_at: string | null;
|
|
49
|
+
metadata?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
interface TeamMemberSnapshot {
|
|
52
|
+
agent_id: string;
|
|
53
|
+
display_name: string;
|
|
54
|
+
role: string;
|
|
55
|
+
}
|
|
56
|
+
export interface WatcherStatePayload {
|
|
57
|
+
fetchedAt: string;
|
|
58
|
+
handoffs: HandoffSnapshot[];
|
|
59
|
+
claims: ClaimSnapshot[];
|
|
60
|
+
blockers: BlockerSnapshot[];
|
|
61
|
+
agents: AgentSnapshot[];
|
|
62
|
+
teamMembers: TeamMemberSnapshot[];
|
|
63
|
+
}
|
|
64
|
+
export declare function fetchWatcherState(ctx: WatcherContext): Promise<WatcherStatePayload>;
|
|
65
|
+
export declare function formatStateForLlm(state: WatcherStatePayload, tickNumber: number, triggerReasons: string[]): string;
|
|
66
|
+
export {};
|