@northflare/runner 0.0.1
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/DEBUG_LOGGING.md +60 -0
- package/LICENSE +21 -0
- package/MIGRATION_PLAN.md +52 -0
- package/README.md +220 -0
- package/SDK_IMPLEMENTATION_GUIDE.md +1036 -0
- package/bin/northflare-runner +367 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/lib/index.html +116 -0
- package/coverage/lib/preload-script.js.html +964 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/src/collections/index.html +116 -0
- package/coverage/src/collections/runner-messages.ts.html +312 -0
- package/coverage/src/components/claude-manager.ts.html +1290 -0
- package/coverage/src/components/index.html +146 -0
- package/coverage/src/components/message-handler.ts.html +730 -0
- package/coverage/src/components/repository-manager.ts.html +841 -0
- package/coverage/src/index.html +131 -0
- package/coverage/src/index.ts.html +448 -0
- package/coverage/src/runner.ts.html +1239 -0
- package/coverage/src/utils/config.ts.html +780 -0
- package/coverage/src/utils/console.ts.html +121 -0
- package/coverage/src/utils/index.html +161 -0
- package/coverage/src/utils/logger.ts.html +475 -0
- package/coverage/src/utils/status-line.ts.html +445 -0
- package/dist/collections/runner-messages.d.ts +52 -0
- package/dist/collections/runner-messages.d.ts.map +1 -0
- package/dist/collections/runner-messages.js +161 -0
- package/dist/collections/runner-messages.js.map +1 -0
- package/dist/components/claude-manager.d.ts +39 -0
- package/dist/components/claude-manager.d.ts.map +1 -0
- package/dist/components/claude-manager.js +783 -0
- package/dist/components/claude-manager.js.map +1 -0
- package/dist/components/claude-sdk-manager.d.ts +47 -0
- package/dist/components/claude-sdk-manager.d.ts.map +1 -0
- package/dist/components/claude-sdk-manager.js +1088 -0
- package/dist/components/claude-sdk-manager.js.map +1 -0
- package/dist/components/enhanced-repository-manager.d.ts +134 -0
- package/dist/components/enhanced-repository-manager.d.ts.map +1 -0
- package/dist/components/enhanced-repository-manager.js +602 -0
- package/dist/components/enhanced-repository-manager.js.map +1 -0
- package/dist/components/message-handler-sse.d.ts +46 -0
- package/dist/components/message-handler-sse.d.ts.map +1 -0
- package/dist/components/message-handler-sse.js +734 -0
- package/dist/components/message-handler-sse.js.map +1 -0
- package/dist/components/message-handler.d.ts +35 -0
- package/dist/components/message-handler.d.ts.map +1 -0
- package/dist/components/message-handler.js +689 -0
- package/dist/components/message-handler.js.map +1 -0
- package/dist/components/repository-manager.d.ts +51 -0
- package/dist/components/repository-manager.d.ts.map +1 -0
- package/dist/components/repository-manager.js +295 -0
- package/dist/components/repository-manager.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/runner-sse.d.ts +57 -0
- package/dist/runner-sse.d.ts.map +1 -0
- package/dist/runner-sse.js +698 -0
- package/dist/runner-sse.js.map +1 -0
- package/dist/runner.d.ts +51 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +530 -0
- package/dist/runner.js.map +1 -0
- package/dist/services/RunnerAPIClient.d.ts +30 -0
- package/dist/services/RunnerAPIClient.d.ts.map +1 -0
- package/dist/services/RunnerAPIClient.js +112 -0
- package/dist/services/RunnerAPIClient.js.map +1 -0
- package/dist/services/SSEClient.d.ts +60 -0
- package/dist/services/SSEClient.d.ts.map +1 -0
- package/dist/services/SSEClient.js +204 -0
- package/dist/services/SSEClient.js.map +1 -0
- package/dist/types/claude.d.ts +45 -0
- package/dist/types/claude.d.ts.map +1 -0
- package/dist/types/claude.js +6 -0
- package/dist/types/claude.js.map +1 -0
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +31 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +6 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/runner-interface.d.ts +24 -0
- package/dist/types/runner-interface.d.ts.map +1 -0
- package/dist/types/runner-interface.js +6 -0
- package/dist/types/runner-interface.js.map +1 -0
- package/dist/utils/StateManager.d.ts +52 -0
- package/dist/utils/StateManager.d.ts.map +1 -0
- package/dist/utils/StateManager.js +162 -0
- package/dist/utils/StateManager.js.map +1 -0
- package/dist/utils/config.d.ts +41 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +250 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/console.d.ts +11 -0
- package/dist/utils/console.d.ts.map +1 -0
- package/dist/utils/console.js +15 -0
- package/dist/utils/console.js.map +1 -0
- package/dist/utils/expand-env.d.ts +2 -0
- package/dist/utils/expand-env.d.ts.map +1 -0
- package/dist/utils/expand-env.js +20 -0
- package/dist/utils/expand-env.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +108 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/status-line.d.ts +37 -0
- package/dist/utils/status-line.d.ts.map +1 -0
- package/dist/utils/status-line.js +113 -0
- package/dist/utils/status-line.js.map +1 -0
- package/docs/claude-manager.md +91 -0
- package/exceptions.log +22 -0
- package/lib/preload-script.js +293 -0
- package/package.json +55 -0
- package/rejections.log +63 -0
- package/runner.log +488 -0
- package/src/components/claude-sdk-manager.ts +1354 -0
- package/src/components/enhanced-repository-manager.ts +823 -0
- package/src/components/message-handler-sse.ts +1011 -0
- package/src/components/repository-manager.ts +337 -0
- package/src/index.ts +166 -0
- package/src/runner-sse.ts +847 -0
- package/src/services/RunnerAPIClient.ts +135 -0
- package/src/services/SSEClient.ts +258 -0
- package/src/types/claude.ts +55 -0
- package/src/types/computer-name.d.ts +4 -0
- package/src/types/index.ts +63 -0
- package/src/types/messages.ts +39 -0
- package/src/types/runner-interface.ts +34 -0
- package/src/utils/StateManager.ts +187 -0
- package/src/utils/codex-sdk.js +448 -0
- package/src/utils/config.ts +315 -0
- package/src/utils/console.ts +13 -0
- package/src/utils/expand-env.ts +22 -0
- package/src/utils/logger.ts +131 -0
- package/src/utils/sdk-demo.js +34 -0
- package/src/utils/status-line.ts +121 -0
- package/test-debug.sh +26 -0
- package/tests/retry-strategies.test.ts +410 -0
- package/tests/sdk-integration.test.ts +329 -0
- package/tests/sdk-streaming.test.ts +1180 -0
- package/tests/setup.ts +5 -0
- package/tests/test-claude-manager.ts +120 -0
- package/tsconfig.json +36 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main RunnerApp class - entry point and lifecycle manager for the runner app
|
|
3
|
+
* Updated to use SSE for real-time messaging instead of ElectricSQL
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
RunnerConfig,
|
|
8
|
+
ConversationContext,
|
|
9
|
+
JsonRpcMessage,
|
|
10
|
+
RetryStrategy,
|
|
11
|
+
RunnerRepo,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { IRunnerApp } from "./types/runner-interface";
|
|
14
|
+
import { MessageHandler } from "./components/message-handler-sse";
|
|
15
|
+
import { ClaudeManager } from "./components/claude-sdk-manager";
|
|
16
|
+
import { EnhancedRepositoryManager } from "./components/enhanced-repository-manager";
|
|
17
|
+
import { StateManager } from "./utils/StateManager";
|
|
18
|
+
import { createLogger } from "./utils/logger";
|
|
19
|
+
import { statusLineManager } from "./utils/status-line";
|
|
20
|
+
import fs from "fs/promises";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import computerName from "computer-name";
|
|
23
|
+
|
|
24
|
+
const logger = createLogger("RunnerApp");
|
|
25
|
+
|
|
26
|
+
export class RunnerApp implements IRunnerApp {
|
|
27
|
+
private messageHandler!: MessageHandler;
|
|
28
|
+
private claudeManager!: ClaudeManager;
|
|
29
|
+
private repositoryManager!: EnhancedRepositoryManager;
|
|
30
|
+
private stateManager!: StateManager;
|
|
31
|
+
private agentConversations: Map<string, ConversationContext>; // Keyed by conversation.id
|
|
32
|
+
private config: RunnerConfig;
|
|
33
|
+
private configPath?: string;
|
|
34
|
+
private heartbeatInterval?: NodeJS.Timeout;
|
|
35
|
+
private isRunning: boolean = false;
|
|
36
|
+
private isRegistered: boolean = false;
|
|
37
|
+
private delayFn: (ms: number) => Promise<void> = (ms) =>
|
|
38
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
|
|
40
|
+
// UID ownership fields
|
|
41
|
+
private runnerUid: string | null = null;
|
|
42
|
+
private lastProcessedAt: Date | null = null;
|
|
43
|
+
private isActiveRunner: boolean = false;
|
|
44
|
+
|
|
45
|
+
// Track conversations started before takeover
|
|
46
|
+
private preHandoffConversations = new Set<string>();
|
|
47
|
+
|
|
48
|
+
constructor(config: RunnerConfig, configPath?: string) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
this.configPath = configPath;
|
|
51
|
+
this.agentConversations = new Map();
|
|
52
|
+
// Note: State manager will be initialized in start() after fetching runnerId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async fetchRunnerId(): Promise<string> {
|
|
56
|
+
const token = process.env["NORTHFLARE_RUNNER_TOKEN"];
|
|
57
|
+
if (!token) {
|
|
58
|
+
throw new Error("NORTHFLARE_RUNNER_TOKEN environment variable is required");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`${this.config.orchestratorUrl}/api/runner/id`, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${token}`,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = await response.json().catch(() => ({ error: response.statusText })) as any;
|
|
71
|
+
throw new Error(`Failed to fetch runner ID: ${error.error || response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = await response.json() as { runnerId?: string };
|
|
75
|
+
if (!data.runnerId) {
|
|
76
|
+
throw new Error("Server did not return runnerId");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.info(`Fetched runnerId: ${data.runnerId}`);
|
|
80
|
+
return data.runnerId;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error("Failed to fetch runner ID from orchestrator", error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private initializeComponents(): void {
|
|
88
|
+
// Initialize repository manager first as it's used by Claude manager
|
|
89
|
+
this.repositoryManager = new EnhancedRepositoryManager(this);
|
|
90
|
+
|
|
91
|
+
// Initialize Claude manager with repository manager (using SDK-native manager)
|
|
92
|
+
this.claudeManager = new ClaudeManager(this, this.repositoryManager);
|
|
93
|
+
|
|
94
|
+
// Initialize message handler with SSE support
|
|
95
|
+
this.messageHandler = new MessageHandler(this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async start(): Promise<void> {
|
|
99
|
+
if (this.isRunning) {
|
|
100
|
+
throw new Error("Runner is already running");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Fetch runnerId from orchestrator using token
|
|
105
|
+
const runnerId = await this.fetchRunnerId();
|
|
106
|
+
this.config.runnerId = runnerId;
|
|
107
|
+
|
|
108
|
+
// Initialize state manager with runnerId
|
|
109
|
+
this.stateManager = new StateManager(this.config.dataDir, runnerId);
|
|
110
|
+
|
|
111
|
+
// Initialize other components
|
|
112
|
+
this.initializeComponents();
|
|
113
|
+
|
|
114
|
+
// Load persisted state
|
|
115
|
+
const savedState = await this.stateManager.loadState();
|
|
116
|
+
if (savedState) {
|
|
117
|
+
// Restore state from previous run
|
|
118
|
+
this.config.runnerId = savedState.runnerId;
|
|
119
|
+
this.runnerUid = savedState.runnerUid;
|
|
120
|
+
this.lastProcessedAt = savedState.lastProcessedAt
|
|
121
|
+
? new Date(savedState.lastProcessedAt)
|
|
122
|
+
: null;
|
|
123
|
+
this.isActiveRunner = savedState.isActiveRunner;
|
|
124
|
+
|
|
125
|
+
logger.info("Restored runner state from disk", {
|
|
126
|
+
runnerId: savedState.runnerId,
|
|
127
|
+
runnerUid: savedState.runnerUid,
|
|
128
|
+
lastProcessedAt: savedState.lastProcessedAt,
|
|
129
|
+
isActiveRunner: savedState.isActiveRunner,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Load runnerRepos for this specific runnerId
|
|
133
|
+
if (this.configPath && savedState.runnerId) {
|
|
134
|
+
await this.loadRunnerReposForId(savedState.runnerId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Register with retry strategy
|
|
139
|
+
await this.registerWithRetry();
|
|
140
|
+
|
|
141
|
+
// Log debug info after registration
|
|
142
|
+
if (process.env["DEBUG"] === "true") {
|
|
143
|
+
logger.debug("Runner initialized with ownership details", {
|
|
144
|
+
runnerId: this.config.runnerId,
|
|
145
|
+
runnerUid: this.runnerUid,
|
|
146
|
+
lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
|
|
147
|
+
isActiveRunner: this.isActiveRunner,
|
|
148
|
+
orchestratorUrl: this.config.orchestratorUrl,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Start message processing with SSE
|
|
153
|
+
await this.messageHandler.startProcessing();
|
|
154
|
+
|
|
155
|
+
// Start heartbeat after successful registration
|
|
156
|
+
this.startHeartbeat();
|
|
157
|
+
|
|
158
|
+
this.isRunning = true;
|
|
159
|
+
|
|
160
|
+
logger.info(`Runner ${this.config.runnerId} started successfully`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error("Failed to start runner:", error);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async stop(): Promise<void> {
|
|
168
|
+
if (!this.isRunning) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logger.info(`Stopping runner ${this.config.runnerId}...`);
|
|
173
|
+
|
|
174
|
+
// Stop heartbeat
|
|
175
|
+
if (this.heartbeatInterval) {
|
|
176
|
+
clearInterval(this.heartbeatInterval);
|
|
177
|
+
this.heartbeatInterval = undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Stop all conversations
|
|
181
|
+
await this.stopAllConversations();
|
|
182
|
+
|
|
183
|
+
// Stop message processing
|
|
184
|
+
await this.messageHandler.stopProcessing();
|
|
185
|
+
|
|
186
|
+
// Clean up status line
|
|
187
|
+
statusLineManager.dispose();
|
|
188
|
+
|
|
189
|
+
this.isRunning = false;
|
|
190
|
+
|
|
191
|
+
logger.info(`Runner ${this.config.runnerId} stopped`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async notify(method: string, params: any): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
// Log RPC notification in debug mode
|
|
197
|
+
logger.debug(`[RPC] Sending notification: ${method}`, {
|
|
198
|
+
method,
|
|
199
|
+
params: JSON.stringify(params, null, 2),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Send notification with retry logic
|
|
203
|
+
await this.sendToOrchestratorWithRetry({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
method,
|
|
206
|
+
params,
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Special handling for heartbeat errors - just log a simple line
|
|
210
|
+
if (method === "runner.heartbeat") {
|
|
211
|
+
const errorMessage =
|
|
212
|
+
error instanceof Error ? error.message : String(error);
|
|
213
|
+
logger.error(`Heartbeat failed: ${errorMessage}`);
|
|
214
|
+
} else {
|
|
215
|
+
// For other RPC errors, log the method and error without stack
|
|
216
|
+
const errorMessage =
|
|
217
|
+
error instanceof Error ? error.message : String(error);
|
|
218
|
+
logger.error(
|
|
219
|
+
`RPC Error: Failed to notify with method '${method}': ${errorMessage}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async sendToOrchestrator(message: JsonRpcMessage): Promise<any> {
|
|
227
|
+
try {
|
|
228
|
+
// Log RPC request in debug mode
|
|
229
|
+
logger.debug(`[RPC] Sending request:`, {
|
|
230
|
+
method: message.method,
|
|
231
|
+
id: message.id,
|
|
232
|
+
params: message.params
|
|
233
|
+
? JSON.stringify(message.params, null, 2)
|
|
234
|
+
: undefined,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Build headers - only include X-Runner-Id if we have one (not during registration)
|
|
238
|
+
const headers: Record<string, string> = {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Only add X-Runner-Id if we're registered (not during registration)
|
|
244
|
+
if (this.config.runnerId && message.method !== "runner.register") {
|
|
245
|
+
headers["X-Runner-Id"] = this.config.runnerId;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const response = await fetch(
|
|
249
|
+
`${this.config.orchestratorUrl}/api/runner/messages`,
|
|
250
|
+
{
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers,
|
|
253
|
+
body: JSON.stringify(message),
|
|
254
|
+
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errorText = await response.text();
|
|
260
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = (await response.json()) as any;
|
|
264
|
+
|
|
265
|
+
// Log RPC response in debug mode
|
|
266
|
+
logger.debug(`[RPC] Received response:`, {
|
|
267
|
+
method: message.method,
|
|
268
|
+
id: message.id,
|
|
269
|
+
result: result?.result
|
|
270
|
+
? JSON.stringify(result.result, null, 2)
|
|
271
|
+
: undefined,
|
|
272
|
+
error: result?.error
|
|
273
|
+
? JSON.stringify(result.error, null, 2)
|
|
274
|
+
: undefined,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const errorMessage =
|
|
280
|
+
error instanceof Error ? error.message : String(error);
|
|
281
|
+
|
|
282
|
+
// Don't log verbose errors for heartbeat failures
|
|
283
|
+
if (message.method === "runner.heartbeat") {
|
|
284
|
+
// Heartbeat errors are already logged in notify()
|
|
285
|
+
} else if (message.method === "runner.register") {
|
|
286
|
+
// Special case for registration failures
|
|
287
|
+
logger.error(`Registration failed: ${errorMessage}`);
|
|
288
|
+
} else {
|
|
289
|
+
// For other RPC messages, log the attempted message and error
|
|
290
|
+
logger.error(`RPC failed:`, {
|
|
291
|
+
method: message.method,
|
|
292
|
+
params: message.params,
|
|
293
|
+
error: errorMessage,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
getConversationContext(
|
|
301
|
+
conversationId: string
|
|
302
|
+
): ConversationContext | undefined {
|
|
303
|
+
// Using conversation.id as primary key for conversation tracking
|
|
304
|
+
const context = this.agentConversations.get(conversationId);
|
|
305
|
+
console.log(`[Runner] getConversationContext lookup:`, {
|
|
306
|
+
conversationId,
|
|
307
|
+
found: !!context,
|
|
308
|
+
totalConversations: this.agentConversations.size,
|
|
309
|
+
allConversationIds: Array.from(this.agentConversations.keys()),
|
|
310
|
+
});
|
|
311
|
+
return context;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async registerRunner(): Promise<void> {
|
|
315
|
+
try {
|
|
316
|
+
// Filter runnerRepos by valid paths
|
|
317
|
+
let filteredRunnerRepos: RunnerRepo[] = [];
|
|
318
|
+
|
|
319
|
+
if (this.config.runnerRepos && Array.isArray(this.config.runnerRepos)) {
|
|
320
|
+
for (const repo of this.config.runnerRepos) {
|
|
321
|
+
try {
|
|
322
|
+
const resolvedPath = path.resolve(repo.path);
|
|
323
|
+
const stats = await fs.stat(resolvedPath);
|
|
324
|
+
if (stats.isDirectory()) {
|
|
325
|
+
filteredRunnerRepos.push(repo);
|
|
326
|
+
logger.info(
|
|
327
|
+
`RunnerRepo validated: ${repo.name} at ${resolvedPath}`
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
logger.error(
|
|
331
|
+
`RunnerRepo path is not a directory: ${repo.name} at ${resolvedPath}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger.error(
|
|
336
|
+
`RunnerRepo path does not exist: ${repo.name} at ${repo.path}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get computer name for registration
|
|
343
|
+
let hostComputerName: string | undefined;
|
|
344
|
+
try {
|
|
345
|
+
hostComputerName = await computerName();
|
|
346
|
+
} catch (error) {
|
|
347
|
+
logger.warn("Failed to get computer name, will use default", error);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const response = await this.sendToOrchestratorWithRetry({
|
|
351
|
+
jsonrpc: "2.0",
|
|
352
|
+
id: "register-" + Date.now(),
|
|
353
|
+
method: "runner.register",
|
|
354
|
+
params: {
|
|
355
|
+
version: "1.0.0",
|
|
356
|
+
runnerRepos: filteredRunnerRepos,
|
|
357
|
+
computerName: hostComputerName,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Debug log the full response
|
|
362
|
+
logger.info(`Registration response:`, JSON.stringify(response, null, 2));
|
|
363
|
+
|
|
364
|
+
// Log the runnerRepos being sent and received
|
|
365
|
+
if (process.env["DEBUG"] === "true") {
|
|
366
|
+
logger.debug(
|
|
367
|
+
"Registration runnerRepos sent:",
|
|
368
|
+
JSON.stringify(filteredRunnerRepos)
|
|
369
|
+
);
|
|
370
|
+
logger.debug(
|
|
371
|
+
"Registration runnerRepos received:",
|
|
372
|
+
JSON.stringify(response?.result?.runnerRepos)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check for JSONRPC error response
|
|
377
|
+
if (response?.error) {
|
|
378
|
+
logger.error(`Registration error from server:`, response.error);
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Server error: ${response.error.message || "Unknown error"}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Extract result from JSONRPC response structure
|
|
385
|
+
const result = response?.result;
|
|
386
|
+
|
|
387
|
+
if (result?.runnerId) {
|
|
388
|
+
this.config.runnerId = result.runnerId;
|
|
389
|
+
this.runnerUid = result.runnerUid;
|
|
390
|
+
this.lastProcessedAt = result.lastProcessedAt
|
|
391
|
+
? new Date(result.lastProcessedAt)
|
|
392
|
+
: null;
|
|
393
|
+
this.isRegistered = true;
|
|
394
|
+
|
|
395
|
+
// Save registration state
|
|
396
|
+
await this.stateManager.updateRunnerRegistration(
|
|
397
|
+
result.runnerId,
|
|
398
|
+
result.runnerUid,
|
|
399
|
+
this.lastProcessedAt
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
logger.info(
|
|
403
|
+
`Runner registered successfully with ID: ${this.config.runnerId} and UID: ${this.runnerUid}`
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Process runnerRepos from response
|
|
407
|
+
if (result.runnerRepos && Array.isArray(result.runnerRepos)) {
|
|
408
|
+
await this.updateRunnerReposWithIds(result.runnerRepos);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Debug logging for registration details
|
|
412
|
+
if (process.env["DEBUG"] === "true") {
|
|
413
|
+
logger.debug("Registration complete with details", {
|
|
414
|
+
runnerId: this.config.runnerId,
|
|
415
|
+
runnerUid: this.runnerUid,
|
|
416
|
+
lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
|
|
417
|
+
orchestratorUrl: this.config.orchestratorUrl,
|
|
418
|
+
dataDir: this.config.dataDir,
|
|
419
|
+
runnerReposCount: result.runnerRepos?.length || 0,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
logger.error(`Registration failed. Expected result.runnerId but got:`, {
|
|
424
|
+
hasResult: !!result,
|
|
425
|
+
resultKeys: result ? Object.keys(result) : [],
|
|
426
|
+
fullResponse: JSON.stringify(response, null, 2),
|
|
427
|
+
});
|
|
428
|
+
throw new Error("Registration response did not include runnerId");
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const errorMessage =
|
|
432
|
+
error instanceof Error ? error.message : String(error);
|
|
433
|
+
logger.error(`Failed to register runner: ${errorMessage}`);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async registerWithRetry(): Promise<void> {
|
|
439
|
+
const strategy = this.config.retryStrategy;
|
|
440
|
+
|
|
441
|
+
if (strategy === "none") {
|
|
442
|
+
// Single attempt, no retries
|
|
443
|
+
await this.registerRunner();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (strategy === "interval") {
|
|
448
|
+
// Retry at fixed intervals
|
|
449
|
+
const intervalMs = this.config.retryIntervalSecs * 1000;
|
|
450
|
+
let attempt = 0;
|
|
451
|
+
|
|
452
|
+
while (!this.isRegistered) {
|
|
453
|
+
attempt++;
|
|
454
|
+
try {
|
|
455
|
+
logger.info(`Registration attempt ${attempt}...`);
|
|
456
|
+
await this.registerRunner();
|
|
457
|
+
return;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const errorMessage =
|
|
460
|
+
error instanceof Error ? error.message : String(error);
|
|
461
|
+
logger.error(
|
|
462
|
+
`Registration attempt ${attempt} failed: ${errorMessage}`
|
|
463
|
+
);
|
|
464
|
+
logger.info(
|
|
465
|
+
`Retrying in ${this.config.retryIntervalSecs} seconds...`
|
|
466
|
+
);
|
|
467
|
+
await this.delayFn(intervalMs);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (strategy === "exponential") {
|
|
473
|
+
// Exponential backoff with max duration
|
|
474
|
+
const maxDurationMs = this.config.retryDurationSecs * 1000;
|
|
475
|
+
const startTime = Date.now();
|
|
476
|
+
let delayMs = 8000; // Start with 8 seconds
|
|
477
|
+
let attempt = 0;
|
|
478
|
+
|
|
479
|
+
while (!this.isRegistered) {
|
|
480
|
+
attempt++;
|
|
481
|
+
try {
|
|
482
|
+
logger.info(`Registration attempt ${attempt}...`);
|
|
483
|
+
await this.registerRunner();
|
|
484
|
+
return;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const errorMessage =
|
|
487
|
+
error instanceof Error ? error.message : String(error);
|
|
488
|
+
logger.error(
|
|
489
|
+
`Registration attempt ${attempt} failed: ${errorMessage}`
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const elapsedMs = Date.now() - startTime;
|
|
493
|
+
if (elapsedMs >= maxDurationMs) {
|
|
494
|
+
// Final retry at max duration
|
|
495
|
+
const remainingMs = maxDurationMs - elapsedMs;
|
|
496
|
+
if (remainingMs > 0) {
|
|
497
|
+
logger.info(
|
|
498
|
+
`Final retry in ${Math.ceil(remainingMs / 1000)} seconds...`
|
|
499
|
+
);
|
|
500
|
+
await this.delayFn(remainingMs);
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
logger.info("Final registration attempt...");
|
|
504
|
+
await this.registerRunner();
|
|
505
|
+
return;
|
|
506
|
+
} catch (finalError) {
|
|
507
|
+
const errorMessage =
|
|
508
|
+
finalError instanceof Error
|
|
509
|
+
? finalError.message
|
|
510
|
+
: String(finalError);
|
|
511
|
+
logger.error(
|
|
512
|
+
`Final registration attempt failed: ${errorMessage}`
|
|
513
|
+
);
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Failed to register after ${this.config.retryDurationSecs} seconds`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Failed to register after ${this.config.retryDurationSecs} seconds`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Calculate next delay with exponential backoff
|
|
526
|
+
const nextDelayMs = Math.min(delayMs, maxDurationMs - elapsedMs);
|
|
527
|
+
logger.info(
|
|
528
|
+
`Retrying in ${Math.ceil(nextDelayMs / 1000)} seconds...`
|
|
529
|
+
);
|
|
530
|
+
await this.delayFn(nextDelayMs);
|
|
531
|
+
|
|
532
|
+
// Double the delay for next attempt
|
|
533
|
+
delayMs = Math.min(delayMs * 2, maxDurationMs);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Load runnerRepos for a specific runnerId from the config file
|
|
541
|
+
*/
|
|
542
|
+
private async loadRunnerReposForId(runnerId: string): Promise<void> {
|
|
543
|
+
if (!this.configPath) {
|
|
544
|
+
logger.debug("No config path specified, cannot load runnerRepos");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const configContent = await fs.readFile(
|
|
550
|
+
path.resolve(this.configPath),
|
|
551
|
+
"utf-8"
|
|
552
|
+
);
|
|
553
|
+
const configFile = JSON.parse(configContent);
|
|
554
|
+
|
|
555
|
+
if (!configFile.runnerRepos || typeof configFile.runnerRepos !== "object") {
|
|
556
|
+
logger.debug("No runnerRepos in config file");
|
|
557
|
+
this.config.runnerRepos = [];
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const reposForThisRunner = configFile.runnerRepos[runnerId];
|
|
562
|
+
if (reposForThisRunner && Array.isArray(reposForThisRunner)) {
|
|
563
|
+
this.config.runnerRepos = reposForThisRunner;
|
|
564
|
+
logger.info(
|
|
565
|
+
`Loaded ${reposForThisRunner.length} runnerRepos for runner ${runnerId}`
|
|
566
|
+
);
|
|
567
|
+
} else {
|
|
568
|
+
this.config.runnerRepos = [];
|
|
569
|
+
logger.debug(
|
|
570
|
+
`No runnerRepos found for runner ${runnerId}, using empty array`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger.error("Failed to load runnerRepos for runner", error);
|
|
575
|
+
this.config.runnerRepos = [];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private async updateRunnerReposWithIds(
|
|
580
|
+
serverRunnerRepos: any[]
|
|
581
|
+
): Promise<void> {
|
|
582
|
+
if (!this.configPath || !this.config.runnerId) {
|
|
583
|
+
logger.info(
|
|
584
|
+
"No config path or runnerId specified, cannot update runnerRepos with IDs"
|
|
585
|
+
);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
const { ConfigManager } = await import("./utils/config");
|
|
591
|
+
const configContent = await fs.readFile(
|
|
592
|
+
path.resolve(this.configPath),
|
|
593
|
+
"utf-8"
|
|
594
|
+
);
|
|
595
|
+
const configFile = JSON.parse(configContent);
|
|
596
|
+
|
|
597
|
+
// Initialize runnerRepos namespace if not present
|
|
598
|
+
if (!configFile.runnerRepos) {
|
|
599
|
+
configFile.runnerRepos = {};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Get or create repos array for this runnerId
|
|
603
|
+
let reposForThisRunner = configFile.runnerRepos[this.config.runnerId];
|
|
604
|
+
if (!reposForThisRunner || !Array.isArray(reposForThisRunner)) {
|
|
605
|
+
reposForThisRunner = [];
|
|
606
|
+
configFile.runnerRepos[this.config.runnerId] = reposForThisRunner;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Update runnerRepos with UUIDs from server
|
|
610
|
+
let hasUpdates = false;
|
|
611
|
+
for (const serverRepo of serverRunnerRepos) {
|
|
612
|
+
const localRepo = reposForThisRunner.find(
|
|
613
|
+
(r: RunnerRepo) =>
|
|
614
|
+
r.name === serverRepo.name && r.path === serverRepo.path
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
if (localRepo && serverRepo.uuid) {
|
|
618
|
+
// Store UUID if not already present or different
|
|
619
|
+
if (!localRepo.uuid || localRepo.uuid !== serverRepo.uuid) {
|
|
620
|
+
localRepo.uuid = serverRepo.uuid;
|
|
621
|
+
hasUpdates = true;
|
|
622
|
+
logger.info(
|
|
623
|
+
`Updated RunnerRepo "${localRepo.name}" with UUID: ${serverRepo.uuid}`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
} else if (serverRepo.uuid) {
|
|
627
|
+
// Add new repo from server
|
|
628
|
+
reposForThisRunner.push({
|
|
629
|
+
uuid: serverRepo.uuid,
|
|
630
|
+
name: serverRepo.name,
|
|
631
|
+
path: serverRepo.path,
|
|
632
|
+
});
|
|
633
|
+
hasUpdates = true;
|
|
634
|
+
logger.info(
|
|
635
|
+
`Added new RunnerRepo "${serverRepo.name}" with UUID: ${serverRepo.uuid}`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Save the updated config if there were changes
|
|
641
|
+
if (hasUpdates) {
|
|
642
|
+
await ConfigManager.saveConfigFile(this.configPath, configFile);
|
|
643
|
+
// Update our in-memory config
|
|
644
|
+
this.config.runnerRepos = reposForThisRunner;
|
|
645
|
+
logger.info(`Saved ${reposForThisRunner.length} runnerRepos for runner ${this.config.runnerId}`);
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
logger.error("Failed to update runnerRepos with IDs", error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private async sendToOrchestratorWithRetry(
|
|
653
|
+
message: JsonRpcMessage
|
|
654
|
+
): Promise<any> {
|
|
655
|
+
const maxRetries = 5;
|
|
656
|
+
const initialDelayMs = 1000; // Start with 1 second
|
|
657
|
+
const maxDelayMs = 60000; // Max 60 seconds
|
|
658
|
+
const jitterMs = 500; // Add up to 500ms of random jitter
|
|
659
|
+
|
|
660
|
+
let lastError: Error | undefined;
|
|
661
|
+
|
|
662
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
663
|
+
try {
|
|
664
|
+
return await this.sendToOrchestrator(message);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
667
|
+
|
|
668
|
+
// Don't retry on certain errors
|
|
669
|
+
if (
|
|
670
|
+
lastError.message.includes("HTTP 401") ||
|
|
671
|
+
lastError.message.includes("HTTP 403") ||
|
|
672
|
+
lastError.message.includes("HTTP 404")
|
|
673
|
+
) {
|
|
674
|
+
throw lastError;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (attempt < maxRetries) {
|
|
678
|
+
// Calculate exponential backoff with jitter
|
|
679
|
+
const baseDelay = Math.min(
|
|
680
|
+
initialDelayMs * Math.pow(2, attempt),
|
|
681
|
+
maxDelayMs
|
|
682
|
+
);
|
|
683
|
+
const jitter = Math.random() * jitterMs;
|
|
684
|
+
const delayMs = baseDelay + jitter;
|
|
685
|
+
|
|
686
|
+
// Log retry attempt
|
|
687
|
+
const errorMessage = lastError.message || String(lastError);
|
|
688
|
+
if (message.method === "runner.heartbeat") {
|
|
689
|
+
// Less verbose for heartbeat retries
|
|
690
|
+
logger.debug(
|
|
691
|
+
`Heartbeat retry ${attempt + 1}/${maxRetries + 1} in ${Math.round(
|
|
692
|
+
delayMs
|
|
693
|
+
)}ms`
|
|
694
|
+
);
|
|
695
|
+
} else {
|
|
696
|
+
logger.warn(
|
|
697
|
+
`RPC retry ${attempt + 1}/${maxRetries + 1} for '${
|
|
698
|
+
message.method
|
|
699
|
+
}' after error: ${errorMessage}. Retrying in ${Math.round(
|
|
700
|
+
delayMs
|
|
701
|
+
)}ms`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
await this.delayFn(delayMs);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// All retries exhausted
|
|
711
|
+
const finalError = lastError || new Error("Unknown error");
|
|
712
|
+
if (message.method !== "runner.heartbeat") {
|
|
713
|
+
logger.error(`RPC failed after ${maxRetries + 1} attempts:`, {
|
|
714
|
+
method: message.method,
|
|
715
|
+
error: finalError.message,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
throw finalError;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private startHeartbeat(): void {
|
|
722
|
+
if (!this.isRegistered || !this.config.runnerId) {
|
|
723
|
+
logger.warn("Cannot start heartbeat: runner not registered");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
728
|
+
try {
|
|
729
|
+
await this.notify("runner.heartbeat", {
|
|
730
|
+
runnerId: this.config.runnerId,
|
|
731
|
+
activeConversations: this.agentConversations.size,
|
|
732
|
+
uptime: process.uptime(),
|
|
733
|
+
memoryUsage: process.memoryUsage(),
|
|
734
|
+
});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
// Error is already logged in notify() method
|
|
737
|
+
}
|
|
738
|
+
}, this.config.heartbeatInterval);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private async stopAllConversations(
|
|
742
|
+
isRunnerShutdown: boolean = true
|
|
743
|
+
): Promise<void> {
|
|
744
|
+
const stopPromises: Promise<void>[] = [];
|
|
745
|
+
|
|
746
|
+
for (const [conversationId, context] of this.agentConversations) {
|
|
747
|
+
if (context.status === "active" || context.status === "starting") {
|
|
748
|
+
stopPromises.push(
|
|
749
|
+
this.claudeManager
|
|
750
|
+
.stopConversation(context.agentSessionId, context, isRunnerShutdown)
|
|
751
|
+
.catch((error) => {
|
|
752
|
+
logger.error(
|
|
753
|
+
`Failed to stop conversation ${context.agentSessionId}:`,
|
|
754
|
+
error
|
|
755
|
+
);
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
await Promise.all(stopPromises);
|
|
762
|
+
this.agentConversations.clear();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Update lastProcessedAt and persist state
|
|
766
|
+
async updateLastProcessedAt(timestamp: Date | null): Promise<void> {
|
|
767
|
+
this.lastProcessedAt = timestamp;
|
|
768
|
+
if (timestamp) {
|
|
769
|
+
await this.stateManager.updateLastProcessedAt(timestamp);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Getters for components
|
|
774
|
+
get config_(): RunnerConfig {
|
|
775
|
+
return this.config;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
get activeConversations_(): Map<string, ConversationContext> {
|
|
779
|
+
return this.agentConversations;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
get claudeManager_(): ClaudeManager {
|
|
783
|
+
return this.claudeManager;
|
|
784
|
+
}
|
|
785
|
+
get repositoryManager_(): EnhancedRepositoryManager {
|
|
786
|
+
return this.repositoryManager;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
getRunnerId(): string | undefined {
|
|
790
|
+
return this.config.runnerId;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// UID ownership getters/setters
|
|
794
|
+
getRunnerUid(): string | null {
|
|
795
|
+
return this.runnerUid;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
getLastProcessedAt(): Date | null {
|
|
799
|
+
return this.lastProcessedAt;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
getIsActiveRunner(): boolean {
|
|
803
|
+
return this.isActiveRunner;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
setIsActiveRunner(active: boolean): void {
|
|
807
|
+
const previousState = this.isActiveRunner;
|
|
808
|
+
this.isActiveRunner = active;
|
|
809
|
+
|
|
810
|
+
// Persist state change
|
|
811
|
+
this.stateManager.updateActiveStatus(active).catch((error) => {
|
|
812
|
+
logger.error("Failed to persist active status:", error);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
if (process.env["DEBUG"] === "true") {
|
|
816
|
+
logger.debug("Active runner status changed", {
|
|
817
|
+
previous: previousState,
|
|
818
|
+
new: active,
|
|
819
|
+
runnerUid: this.runnerUid,
|
|
820
|
+
lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
setLastProcessedAt(timestamp: Date | null): void {
|
|
826
|
+
const previousTimestamp = this.lastProcessedAt;
|
|
827
|
+
this.lastProcessedAt = timestamp;
|
|
828
|
+
|
|
829
|
+
if (process.env["DEBUG"] === "true") {
|
|
830
|
+
logger.debug("LastProcessedAt updated", {
|
|
831
|
+
previous: previousTimestamp?.toISOString() || "null",
|
|
832
|
+
new: timestamp?.toISOString() || "null",
|
|
833
|
+
runnerUid: this.runnerUid,
|
|
834
|
+
isActiveRunner: this.isActiveRunner,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
getPreHandoffConversations(): Set<string> {
|
|
840
|
+
return this.preHandoffConversations;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// For testing purposes - allows mocking delays
|
|
844
|
+
setDelayFunction(fn: (ms: number) => Promise<void>): void {
|
|
845
|
+
this.delayFn = fn;
|
|
846
|
+
}
|
|
847
|
+
}
|