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