@northflare/runner 0.0.30 → 0.0.32

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 (119) hide show
  1. package/bin/northflare-runner +1 -1
  2. package/dist/chunk-3QTLJ4CG.js +33622 -0
  3. package/dist/chunk-3QTLJ4CG.js.map +1 -0
  4. package/dist/chunk-7D4SUZUM.js +38 -0
  5. package/dist/chunk-7D4SUZUM.js.map +1 -0
  6. package/dist/dist-W7DZRE4U.js +365 -0
  7. package/dist/dist-W7DZRE4U.js.map +1 -0
  8. package/dist/index.d.ts +764 -5
  9. package/dist/index.js +9872 -202
  10. package/dist/index.js.map +1 -1
  11. package/dist/sdk-query-TRMSGGID-EIENWDKW.js +14 -0
  12. package/dist/sdk-query-TRMSGGID-EIENWDKW.js.map +1 -0
  13. package/package.json +17 -17
  14. package/tsup.config.ts +5 -2
  15. package/dist/components/claude-sdk-manager.d.ts +0 -60
  16. package/dist/components/claude-sdk-manager.d.ts.map +0 -1
  17. package/dist/components/claude-sdk-manager.js +0 -1378
  18. package/dist/components/claude-sdk-manager.js.map +0 -1
  19. package/dist/components/codex-sdk-manager.d.ts +0 -94
  20. package/dist/components/codex-sdk-manager.d.ts.map +0 -1
  21. package/dist/components/codex-sdk-manager.js +0 -1450
  22. package/dist/components/codex-sdk-manager.js.map +0 -1
  23. package/dist/components/enhanced-repository-manager.d.ts +0 -173
  24. package/dist/components/enhanced-repository-manager.d.ts.map +0 -1
  25. package/dist/components/enhanced-repository-manager.js +0 -1097
  26. package/dist/components/enhanced-repository-manager.js.map +0 -1
  27. package/dist/components/message-handler-sse.d.ts +0 -77
  28. package/dist/components/message-handler-sse.d.ts.map +0 -1
  29. package/dist/components/message-handler-sse.js +0 -1224
  30. package/dist/components/message-handler-sse.js.map +0 -1
  31. package/dist/components/northflare-agent-sdk-manager.d.ts +0 -58
  32. package/dist/components/northflare-agent-sdk-manager.d.ts.map +0 -1
  33. package/dist/components/northflare-agent-sdk-manager.js +0 -2032
  34. package/dist/components/northflare-agent-sdk-manager.js.map +0 -1
  35. package/dist/components/repository-manager.d.ts +0 -51
  36. package/dist/components/repository-manager.d.ts.map +0 -1
  37. package/dist/components/repository-manager.js +0 -256
  38. package/dist/components/repository-manager.js.map +0 -1
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/runner-sse.d.ts +0 -102
  41. package/dist/runner-sse.d.ts.map +0 -1
  42. package/dist/runner-sse.js +0 -877
  43. package/dist/runner-sse.js.map +0 -1
  44. package/dist/services/RunnerAPIClient.d.ts +0 -61
  45. package/dist/services/RunnerAPIClient.d.ts.map +0 -1
  46. package/dist/services/RunnerAPIClient.js +0 -187
  47. package/dist/services/RunnerAPIClient.js.map +0 -1
  48. package/dist/services/SSEClient.d.ts +0 -62
  49. package/dist/services/SSEClient.d.ts.map +0 -1
  50. package/dist/services/SSEClient.js +0 -225
  51. package/dist/services/SSEClient.js.map +0 -1
  52. package/dist/types/claude.d.ts +0 -80
  53. package/dist/types/claude.d.ts.map +0 -1
  54. package/dist/types/claude.js +0 -5
  55. package/dist/types/claude.js.map +0 -1
  56. package/dist/types/index.d.ts +0 -52
  57. package/dist/types/index.d.ts.map +0 -1
  58. package/dist/types/index.js +0 -7
  59. package/dist/types/index.js.map +0 -1
  60. package/dist/types/messages.d.ts +0 -33
  61. package/dist/types/messages.d.ts.map +0 -1
  62. package/dist/types/messages.js +0 -5
  63. package/dist/types/messages.js.map +0 -1
  64. package/dist/types/runner-interface.d.ts +0 -38
  65. package/dist/types/runner-interface.d.ts.map +0 -1
  66. package/dist/types/runner-interface.js +0 -5
  67. package/dist/types/runner-interface.js.map +0 -1
  68. package/dist/utils/StateManager.d.ts +0 -61
  69. package/dist/utils/StateManager.d.ts.map +0 -1
  70. package/dist/utils/StateManager.js +0 -170
  71. package/dist/utils/StateManager.js.map +0 -1
  72. package/dist/utils/config.d.ts +0 -48
  73. package/dist/utils/config.d.ts.map +0 -1
  74. package/dist/utils/config.js +0 -378
  75. package/dist/utils/config.js.map +0 -1
  76. package/dist/utils/console.d.ts +0 -8
  77. package/dist/utils/console.d.ts.map +0 -1
  78. package/dist/utils/console.js +0 -31
  79. package/dist/utils/console.js.map +0 -1
  80. package/dist/utils/debug.d.ts +0 -12
  81. package/dist/utils/debug.d.ts.map +0 -1
  82. package/dist/utils/debug.js +0 -94
  83. package/dist/utils/debug.js.map +0 -1
  84. package/dist/utils/expand-env.d.ts +0 -2
  85. package/dist/utils/expand-env.d.ts.map +0 -1
  86. package/dist/utils/expand-env.js +0 -17
  87. package/dist/utils/expand-env.js.map +0 -1
  88. package/dist/utils/inactivity-timeout.d.ts +0 -19
  89. package/dist/utils/inactivity-timeout.d.ts.map +0 -1
  90. package/dist/utils/inactivity-timeout.js +0 -72
  91. package/dist/utils/inactivity-timeout.js.map +0 -1
  92. package/dist/utils/logger.d.ts +0 -10
  93. package/dist/utils/logger.d.ts.map +0 -1
  94. package/dist/utils/logger.js +0 -129
  95. package/dist/utils/logger.js.map +0 -1
  96. package/dist/utils/message-log.d.ts +0 -23
  97. package/dist/utils/message-log.d.ts.map +0 -1
  98. package/dist/utils/message-log.js +0 -69
  99. package/dist/utils/message-log.js.map +0 -1
  100. package/dist/utils/model.d.ts +0 -8
  101. package/dist/utils/model.d.ts.map +0 -1
  102. package/dist/utils/model.js +0 -37
  103. package/dist/utils/model.js.map +0 -1
  104. package/dist/utils/status-line.d.ts +0 -34
  105. package/dist/utils/status-line.d.ts.map +0 -1
  106. package/dist/utils/status-line.js +0 -131
  107. package/dist/utils/status-line.js.map +0 -1
  108. package/dist/utils/tool-response-sanitizer.d.ts +0 -9
  109. package/dist/utils/tool-response-sanitizer.d.ts.map +0 -1
  110. package/dist/utils/tool-response-sanitizer.js +0 -118
  111. package/dist/utils/tool-response-sanitizer.js.map +0 -1
  112. package/dist/utils/update-coordinator.d.ts +0 -53
  113. package/dist/utils/update-coordinator.d.ts.map +0 -1
  114. package/dist/utils/update-coordinator.js +0 -159
  115. package/dist/utils/update-coordinator.js.map +0 -1
  116. package/dist/utils/version.d.ts +0 -10
  117. package/dist/utils/version.d.ts.map +0 -1
  118. package/dist/utils/version.js +0 -33
  119. package/dist/utils/version.js.map +0 -1
@@ -1,877 +0,0 @@
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
- import { MessageHandler } from './components/message-handler-sse.js';
6
- import { ClaudeManager } from './components/claude-sdk-manager.js';
7
- import { CodexManager } from './components/codex-sdk-manager.js';
8
- import { NorthflareAgentManager } from './components/northflare-agent-sdk-manager.js';
9
- import { EnhancedRepositoryManager } from './components/enhanced-repository-manager.js';
10
- import { StateManager } from './utils/StateManager.js';
11
- import { createLogger } from './utils/logger.js';
12
- import { statusLineManager } from './utils/status-line.js';
13
- import { isRunnerDebugEnabled } from './utils/debug.js';
14
- import fs from "fs/promises";
15
- import path from "path";
16
- import computerName from "computer-name";
17
- import { sanitizeToolResponsePayload, TOOL_RESPONSE_BYTE_LIMIT, } from './utils/tool-response-sanitizer.js';
18
- import { buildOutgoingMessageLog, messageFlowLogger, messagesDebugEnabled, } from './utils/message-log.js';
19
- import { RunnerAPIClient } from './services/RunnerAPIClient.js';
20
- import { UpdateCoordinator } from './utils/update-coordinator.js';
21
- import { RUNNER_VERSION } from './utils/version.js';
22
- function isSubPath(base, candidate) {
23
- if (!base)
24
- return false;
25
- const normalizedBase = path.resolve(base);
26
- const normalizedCandidate = path.resolve(candidate);
27
- const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
28
- return (normalizedCandidate === normalizedBase ||
29
- normalizedCandidate.startsWith(baseWithSep));
30
- }
31
- const logger = createLogger("RunnerApp");
32
- function logToolResponseTruncation(toolUseId) {
33
- const message = `Tool response exceeded ${TOOL_RESPONSE_BYTE_LIMIT} bytes and was truncated`;
34
- if (toolUseId) {
35
- logger.warn(message, { toolUseId });
36
- }
37
- else {
38
- logger.warn(message);
39
- }
40
- }
41
- export class RunnerApp {
42
- messageHandler;
43
- claudeManager;
44
- codexManager;
45
- northflareAgentManager;
46
- repositoryManager;
47
- stateManager;
48
- updateCoordinator;
49
- agentConversations; // Keyed by conversation.id
50
- config;
51
- configPath;
52
- runnerReposCachePath;
53
- heartbeatInterval;
54
- heartbeatInFlight = false;
55
- isRunning = false;
56
- isRegistered = false;
57
- delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
58
- // UID ownership fields
59
- runnerUid = null;
60
- lastProcessedAt = null;
61
- isActiveRunner = false;
62
- // Track conversations started before takeover
63
- preHandoffConversations = new Set();
64
- // Track recently completed conversation IDs to prevent restart on catch-up
65
- // Uses a Map with completion timestamp to allow cleanup of old entries
66
- completedConversations = new Map();
67
- static COMPLETED_CONVERSATION_TTL_MS = 60 * 60 * 1000; // 1 hour
68
- // Track summaries that arrive before a conversation starts.
69
- pendingConversationSummaries = new Map();
70
- constructor(config, configPath) {
71
- this.config = config;
72
- this.configPath = configPath;
73
- this.agentConversations = new Map();
74
- this.runnerReposCachePath = path.join(this.config.dataDir, "runner-repos-cache.json");
75
- // Note: State manager will be initialized in start() after fetching runnerId
76
- }
77
- async fetchRunnerId() {
78
- const token = process.env["NORTHFLARE_RUNNER_TOKEN"];
79
- if (!token) {
80
- throw new Error("NORTHFLARE_RUNNER_TOKEN environment variable is required");
81
- }
82
- try {
83
- const response = await fetch(`${this.config.orchestratorUrl}/api/runner/id`, {
84
- method: "GET",
85
- headers: {
86
- Authorization: `Bearer ${token}`,
87
- },
88
- });
89
- if (!response.ok) {
90
- const error = await response.json().catch(() => ({ error: response.statusText }));
91
- throw new Error(`Failed to fetch runner ID: ${error.error || response.statusText}`);
92
- }
93
- const data = await response.json();
94
- if (!data.runnerId) {
95
- throw new Error("Server did not return runnerId");
96
- }
97
- logger.info(`Fetched runnerId: ${data.runnerId}`);
98
- return data.runnerId;
99
- }
100
- catch (error) {
101
- logger.error("Failed to fetch runner ID from orchestrator", error);
102
- throw error;
103
- }
104
- }
105
- initializeComponents() {
106
- // Initialize repository manager first as it's used by Claude manager
107
- this.repositoryManager = new EnhancedRepositoryManager(this);
108
- // Initialize Claude manager with repository manager (using SDK-native manager)
109
- this.claudeManager = new ClaudeManager(this, this.repositoryManager);
110
- // Initialize Codex manager for OpenAI-based conversations
111
- this.codexManager = new CodexManager(this, this.repositoryManager);
112
- // Initialize Northflare Agent manager for OpenRouter-based conversations
113
- this.northflareAgentManager = new NorthflareAgentManager(this, this.repositoryManager);
114
- // Initialize message handler with SSE support
115
- this.messageHandler = new MessageHandler(this);
116
- // Initialize update coordinator for auto-updates
117
- this.updateCoordinator = new UpdateCoordinator({
118
- autoUpdateDisabled: process.env["NORTHFLARE_DISABLE_AUTO_UPDATE"] === 'true',
119
- getActiveConversationCount: () => this.agentConversations.size,
120
- saveState: async () => {
121
- await this.stateManager.saveState({
122
- runnerId: this.config.runnerId,
123
- runnerUid: this.runnerUid,
124
- lastProcessedAt: this.lastProcessedAt?.toISOString() || null,
125
- isActiveRunner: this.isActiveRunner,
126
- updatedAt: new Date().toISOString(),
127
- });
128
- },
129
- getConfigPath: () => this.configPath,
130
- });
131
- }
132
- async start() {
133
- if (this.isRunning) {
134
- throw new Error("Runner is already running");
135
- }
136
- try {
137
- // Fetch runnerId from orchestrator using token
138
- const runnerId = await this.fetchRunnerId();
139
- this.config.runnerId = runnerId;
140
- // Initialize state manager with runnerId
141
- this.stateManager = new StateManager(this.config.dataDir, runnerId);
142
- // Initialize other components
143
- this.initializeComponents();
144
- // Load persisted state
145
- const savedState = await this.stateManager.loadState();
146
- if (savedState) {
147
- // Restore state from previous run
148
- this.config.runnerId = savedState.runnerId;
149
- this.runnerUid = savedState.runnerUid;
150
- this.lastProcessedAt = savedState.lastProcessedAt
151
- ? new Date(savedState.lastProcessedAt)
152
- : null;
153
- this.isActiveRunner = savedState.isActiveRunner;
154
- logger.info("Restored runner state from disk", {
155
- runnerId: savedState.runnerId,
156
- runnerUid: savedState.runnerUid,
157
- lastProcessedAt: savedState.lastProcessedAt,
158
- isActiveRunner: savedState.isActiveRunner,
159
- });
160
- }
161
- // Bootstrap runner repos from server (with cache fallback)
162
- await this.refreshRunnerReposFromServer(true);
163
- // Register with retry strategy
164
- await this.registerWithRetry();
165
- // Log debug info after registration
166
- if (isRunnerDebugEnabled()) {
167
- logger.debug("Runner initialized with ownership details", {
168
- runnerId: this.config.runnerId,
169
- runnerUid: this.runnerUid,
170
- lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
171
- isActiveRunner: this.isActiveRunner,
172
- orchestratorUrl: this.config.orchestratorUrl,
173
- });
174
- }
175
- // Start message processing with SSE
176
- await this.messageHandler.startProcessing();
177
- // Start heartbeat after successful registration
178
- this.startHeartbeat();
179
- this.isRunning = true;
180
- logger.info(`Runner ${this.config.runnerId} started successfully`);
181
- }
182
- catch (error) {
183
- logger.error("Failed to start runner:", error);
184
- throw error;
185
- }
186
- }
187
- async stop() {
188
- if (!this.isRunning) {
189
- return;
190
- }
191
- logger.info(`Stopping runner ${this.config.runnerId}...`);
192
- // Stop heartbeat
193
- if (this.heartbeatInterval) {
194
- clearInterval(this.heartbeatInterval);
195
- this.heartbeatInterval = undefined;
196
- }
197
- // Stop all conversations
198
- await this.stopAllConversations();
199
- // Stop message processing
200
- await this.messageHandler.stopProcessing();
201
- // Clean up status line
202
- statusLineManager.dispose();
203
- this.isRunning = false;
204
- logger.info(`Runner ${this.config.runnerId} stopped`);
205
- }
206
- async notify(method, params) {
207
- try {
208
- const sanitization = sanitizeToolResponsePayload(method, params);
209
- if (sanitization.truncated) {
210
- logToolResponseTruncation(sanitization.toolUseId);
211
- }
212
- const safeParams = sanitization.truncated
213
- ? sanitization.params
214
- : params;
215
- // Log RPC notification in debug mode
216
- logger.debug(`[RPC] Sending notification: ${method}`, {
217
- method,
218
- params: JSON.stringify(safeParams, null, 2),
219
- });
220
- // Send notification with retry logic
221
- await this.sendToOrchestratorWithRetry({
222
- jsonrpc: "2.0",
223
- method,
224
- params: safeParams,
225
- });
226
- }
227
- catch (error) {
228
- // Special handling for heartbeat errors - just log a simple line
229
- if (method === "runner.heartbeat") {
230
- const errorMessage = error instanceof Error ? error.message : String(error);
231
- logger.error(`Heartbeat failed: ${errorMessage}`);
232
- }
233
- else {
234
- // For other RPC errors, log the method and error without stack
235
- const errorMessage = error instanceof Error ? error.message : String(error);
236
- logger.error(`RPC Error: Failed to notify with method '${method}': ${errorMessage}`);
237
- }
238
- throw error;
239
- }
240
- }
241
- async sendToOrchestrator(message) {
242
- const sanitization = sanitizeToolResponsePayload(message.method, message.params);
243
- if (sanitization.truncated) {
244
- logToolResponseTruncation(sanitization.toolUseId);
245
- }
246
- const messageToSend = sanitization.truncated
247
- ? {
248
- ...message,
249
- params: sanitization.params,
250
- }
251
- : message;
252
- try {
253
- if (messagesDebugEnabled()) {
254
- messageFlowLogger.debug("[outgoing] runner -> orchestrator", {
255
- runnerId: this.config.runnerId,
256
- ...buildOutgoingMessageLog(messageToSend),
257
- });
258
- }
259
- // Log RPC request in debug mode
260
- logger.debug(`[RPC] Sending request:`, {
261
- method: messageToSend.method,
262
- id: messageToSend.id,
263
- params: messageToSend.params
264
- ? JSON.stringify(messageToSend.params, null, 2)
265
- : undefined,
266
- });
267
- // Build headers - only include X-Runner-Id if we have one (not during registration)
268
- const headers = {
269
- "Content-Type": "application/json",
270
- Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
271
- };
272
- // Only add X-Runner-Id if we're registered (not during registration)
273
- if (this.config.runnerId && message.method !== "runner.register") {
274
- headers["X-Runner-Id"] = this.config.runnerId;
275
- }
276
- const response = await fetch(`${this.config.orchestratorUrl}/api/runner/messages`, {
277
- method: "POST",
278
- headers,
279
- body: JSON.stringify(messageToSend),
280
- signal: AbortSignal.timeout(30000), // 30 second timeout
281
- });
282
- if (!response.ok) {
283
- const errorText = await response.text();
284
- throw new Error(`HTTP ${response.status}: ${errorText}`);
285
- }
286
- const result = (await response.json());
287
- // Log RPC response in debug mode
288
- logger.debug(`[RPC] Received response:`, {
289
- method: messageToSend.method,
290
- id: messageToSend.id,
291
- result: result?.result
292
- ? JSON.stringify(result.result, null, 2)
293
- : undefined,
294
- error: result?.error
295
- ? JSON.stringify(result.error, null, 2)
296
- : undefined,
297
- });
298
- return result;
299
- }
300
- catch (error) {
301
- const errorMessage = error instanceof Error ? error.message : String(error);
302
- // Don't log verbose errors for heartbeat failures
303
- if (message.method === "runner.heartbeat") {
304
- // Heartbeat errors are already logged in notify()
305
- }
306
- else if (message.method === "runner.register") {
307
- // Special case for registration failures
308
- logger.error(`Registration failed: ${errorMessage}`);
309
- }
310
- else {
311
- // For other RPC messages, log the attempted message and error
312
- logger.error(`RPC failed:`, {
313
- method: messageToSend.method,
314
- params: messageToSend.params,
315
- error: errorMessage,
316
- });
317
- }
318
- throw error;
319
- }
320
- }
321
- getConversationContext(conversationId) {
322
- // Using conversation.id as primary key for conversation tracking
323
- const context = this.agentConversations.get(conversationId);
324
- logger.debug(`[Runner] getConversationContext lookup:`, {
325
- conversationId,
326
- found: !!context,
327
- totalConversations: this.agentConversations.size,
328
- allConversationIds: Array.from(this.agentConversations.keys()),
329
- });
330
- return context;
331
- }
332
- applyConversationSummary(conversationId, summary) {
333
- const normalizedSummary = typeof summary === "string" ? summary.replace(/\s+/g, " ").trim() : "";
334
- if (!normalizedSummary)
335
- return;
336
- const context = this.agentConversations.get(conversationId);
337
- if (context) {
338
- context.summary = normalizedSummary;
339
- return;
340
- }
341
- this.pendingConversationSummaries.set(conversationId, normalizedSummary);
342
- }
343
- consumePendingConversationSummary(conversationId) {
344
- const summary = this.pendingConversationSummaries.get(conversationId);
345
- if (summary) {
346
- this.pendingConversationSummaries.delete(conversationId);
347
- }
348
- return summary;
349
- }
350
- /**
351
- * Check if a conversation was recently completed (to prevent restart on catch-up)
352
- */
353
- wasConversationCompleted(conversationId) {
354
- const completedAt = this.completedConversations.get(conversationId);
355
- if (!completedAt)
356
- return false;
357
- // Check if the completion is still within TTL
358
- const now = Date.now();
359
- if (now - completedAt > RunnerApp.COMPLETED_CONVERSATION_TTL_MS) {
360
- // Expired, clean it up
361
- this.completedConversations.delete(conversationId);
362
- return false;
363
- }
364
- return true;
365
- }
366
- /**
367
- * Mark a conversation as completed to prevent restart on catch-up
368
- */
369
- markConversationCompleted(conversationId) {
370
- this.completedConversations.set(conversationId, Date.now());
371
- // Opportunistically clean up old entries
372
- this.cleanupCompletedConversations();
373
- }
374
- /**
375
- * Clean up expired completed conversation entries
376
- */
377
- cleanupCompletedConversations() {
378
- const now = Date.now();
379
- for (const [id, completedAt] of this.completedConversations) {
380
- if (now - completedAt > RunnerApp.COMPLETED_CONVERSATION_TTL_MS) {
381
- this.completedConversations.delete(id);
382
- }
383
- }
384
- }
385
- async registerRunner() {
386
- try {
387
- if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) {
388
- throw new Error("workspacePath must be an absolute path");
389
- }
390
- const filteredRunnerRepos = Array.isArray(this.config.runnerRepos)
391
- ? this.config.runnerRepos
392
- : [];
393
- // Get computer name for registration
394
- let hostComputerName;
395
- try {
396
- hostComputerName = await computerName();
397
- }
398
- catch (error) {
399
- logger.warn("Failed to get computer name, will use default", error);
400
- }
401
- const response = await this.sendToOrchestratorWithRetry({
402
- jsonrpc: "2.0",
403
- id: "register-" + Date.now(),
404
- method: "runner.register",
405
- params: {
406
- version: RUNNER_VERSION,
407
- runnerRepos: filteredRunnerRepos,
408
- computerName: hostComputerName,
409
- workspacePath: this.config.workspacePath,
410
- },
411
- });
412
- // Debug log the full response
413
- logger.info(`Registration response:`, JSON.stringify(response, null, 2));
414
- // Log the runnerRepos being sent and received
415
- if (isRunnerDebugEnabled()) {
416
- logger.debug("Registration runnerRepos sent:", JSON.stringify(filteredRunnerRepos));
417
- logger.debug("Registration runnerRepos received:", JSON.stringify(response?.result?.runnerRepos));
418
- }
419
- // Check for JSONRPC error response
420
- if (response?.error) {
421
- logger.error(`Registration error from server:`, response.error);
422
- throw new Error(`Server error: ${response.error.message || "Unknown error"}`);
423
- }
424
- // Extract result from JSONRPC response structure
425
- const result = response?.result;
426
- if (result?.runnerId) {
427
- this.config.runnerId = result.runnerId;
428
- this.runnerUid = result.runnerUid;
429
- this.lastProcessedAt = result.lastProcessedAt
430
- ? new Date(result.lastProcessedAt)
431
- : null;
432
- this.isRegistered = true;
433
- // If this is a fresh start (no previous lastProcessedAt), set as active runner immediately
434
- // The server will also send a runner.uid.changed message, but we set it here to avoid
435
- // any window where isActiveRunner is incorrectly false
436
- if (!result.lastProcessedAt) {
437
- this.isActiveRunner = true;
438
- logger.info(`First-time registration - setting as active runner immediately`);
439
- }
440
- // Save registration state
441
- await this.stateManager.updateRunnerRegistration(result.runnerId, result.runnerUid, this.lastProcessedAt);
442
- // Persist the active status if we set it
443
- if (this.isActiveRunner) {
444
- await this.stateManager.updateActiveStatus(true);
445
- }
446
- logger.info(`Runner registered successfully with ID: ${this.config.runnerId} and UID: ${this.runnerUid}`);
447
- // Process runnerRepos from response
448
- if (result.runnerRepos && Array.isArray(result.runnerRepos)) {
449
- const prepared = await this.prepareRunnerRepos(result.runnerRepos);
450
- await this.replaceRunnerRepos(prepared);
451
- }
452
- // Debug logging for registration details
453
- if (isRunnerDebugEnabled()) {
454
- logger.debug("Registration complete with details", {
455
- runnerId: this.config.runnerId,
456
- runnerUid: this.runnerUid,
457
- lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
458
- orchestratorUrl: this.config.orchestratorUrl,
459
- dataDir: this.config.dataDir,
460
- runnerReposCount: result.runnerRepos?.length || 0,
461
- });
462
- }
463
- }
464
- else {
465
- logger.error(`Registration failed. Expected result.runnerId but got:`, {
466
- hasResult: !!result,
467
- resultKeys: result ? Object.keys(result) : [],
468
- fullResponse: JSON.stringify(response, null, 2),
469
- });
470
- throw new Error("Registration response did not include runnerId");
471
- }
472
- }
473
- catch (error) {
474
- const errorMessage = error instanceof Error ? error.message : String(error);
475
- logger.error(`Failed to register runner: ${errorMessage}`);
476
- throw error;
477
- }
478
- }
479
- async registerWithRetry() {
480
- const strategy = this.config.retryStrategy;
481
- if (strategy === "none") {
482
- // Single attempt, no retries
483
- await this.registerRunner();
484
- return;
485
- }
486
- if (strategy === "interval") {
487
- // Retry at fixed intervals
488
- const intervalMs = this.config.retryIntervalSecs * 1000;
489
- let attempt = 0;
490
- while (!this.isRegistered) {
491
- attempt++;
492
- try {
493
- logger.info(`Registration attempt ${attempt}...`);
494
- await this.registerRunner();
495
- return;
496
- }
497
- catch (error) {
498
- const errorMessage = error instanceof Error ? error.message : String(error);
499
- logger.error(`Registration attempt ${attempt} failed: ${errorMessage}`);
500
- logger.info(`Retrying in ${this.config.retryIntervalSecs} seconds...`);
501
- await this.delayFn(intervalMs);
502
- }
503
- }
504
- }
505
- if (strategy === "exponential") {
506
- // Exponential backoff with max duration
507
- const maxDurationMs = this.config.retryDurationSecs * 1000;
508
- const startTime = Date.now();
509
- let delayMs = 8000; // Start with 8 seconds
510
- let attempt = 0;
511
- while (!this.isRegistered) {
512
- attempt++;
513
- try {
514
- logger.info(`Registration attempt ${attempt}...`);
515
- await this.registerRunner();
516
- return;
517
- }
518
- catch (error) {
519
- const errorMessage = error instanceof Error ? error.message : String(error);
520
- logger.error(`Registration attempt ${attempt} failed: ${errorMessage}`);
521
- const elapsedMs = Date.now() - startTime;
522
- if (elapsedMs >= maxDurationMs) {
523
- // Final retry at max duration
524
- const remainingMs = maxDurationMs - elapsedMs;
525
- if (remainingMs > 0) {
526
- logger.info(`Final retry in ${Math.ceil(remainingMs / 1000)} seconds...`);
527
- await this.delayFn(remainingMs);
528
- try {
529
- logger.info("Final registration attempt...");
530
- await this.registerRunner();
531
- return;
532
- }
533
- catch (finalError) {
534
- const errorMessage = finalError instanceof Error
535
- ? finalError.message
536
- : String(finalError);
537
- logger.error(`Final registration attempt failed: ${errorMessage}`);
538
- throw new Error(`Failed to register after ${this.config.retryDurationSecs} seconds`);
539
- }
540
- }
541
- else {
542
- throw new Error(`Failed to register after ${this.config.retryDurationSecs} seconds`);
543
- }
544
- }
545
- // Calculate next delay with exponential backoff
546
- const nextDelayMs = Math.min(delayMs, maxDurationMs - elapsedMs);
547
- logger.info(`Retrying in ${Math.ceil(nextDelayMs / 1000)} seconds...`);
548
- await this.delayFn(nextDelayMs);
549
- // Double the delay for next attempt
550
- delayMs = Math.min(delayMs * 2, maxDurationMs);
551
- }
552
- }
553
- }
554
- }
555
- async sendToOrchestratorWithRetry(message) {
556
- const maxRetries = 5;
557
- const initialDelayMs = 1000; // Start with 1 second
558
- const maxDelayMs = 60000; // Max 60 seconds
559
- const jitterMs = 500; // Add up to 500ms of random jitter
560
- let lastError;
561
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
562
- try {
563
- return await this.sendToOrchestrator(message);
564
- }
565
- catch (error) {
566
- lastError = error instanceof Error ? error : new Error(String(error));
567
- // Don't retry on certain errors
568
- if (lastError.message.includes("HTTP 401") ||
569
- lastError.message.includes("HTTP 403") ||
570
- lastError.message.includes("HTTP 404")) {
571
- throw lastError;
572
- }
573
- if (attempt < maxRetries) {
574
- // Calculate exponential backoff with jitter
575
- const baseDelay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
576
- const jitter = Math.random() * jitterMs;
577
- const delayMs = baseDelay + jitter;
578
- // Log retry attempt
579
- const errorMessage = lastError.message || String(lastError);
580
- if (message.method === "runner.heartbeat") {
581
- // Less verbose for heartbeat retries
582
- logger.debug(`Heartbeat retry ${attempt + 1}/${maxRetries + 1} in ${Math.round(delayMs)}ms`);
583
- }
584
- else {
585
- logger.warn(`RPC retry ${attempt + 1}/${maxRetries + 1} for '${message.method}' after error: ${errorMessage}. Retrying in ${Math.round(delayMs)}ms`);
586
- }
587
- await this.delayFn(delayMs);
588
- }
589
- }
590
- }
591
- // All retries exhausted
592
- const finalError = lastError || new Error("Unknown error");
593
- if (message.method !== "runner.heartbeat") {
594
- logger.error(`RPC failed after ${maxRetries + 1} attempts:`, {
595
- method: message.method,
596
- error: finalError.message,
597
- });
598
- }
599
- throw finalError;
600
- }
601
- startHeartbeat() {
602
- if (!this.isRegistered || !this.config.runnerId) {
603
- logger.warn("Cannot start heartbeat: runner not registered");
604
- return;
605
- }
606
- this.heartbeatInterval = setInterval(async () => {
607
- if (this.heartbeatInFlight) {
608
- return;
609
- }
610
- this.heartbeatInFlight = true;
611
- try {
612
- const response = await this.sendToOrchestrator({
613
- jsonrpc: "2.0",
614
- id: `heartbeat-${Date.now()}`,
615
- method: "runner.heartbeat",
616
- params: {
617
- runnerId: this.config.runnerId,
618
- activeConversations: this.agentConversations.size,
619
- uptime: process.uptime(),
620
- memoryUsage: process.memoryUsage(),
621
- },
622
- });
623
- // Check for expectedRunnerVersion in heartbeat response
624
- if (response?.result?.expectedRunnerVersion) {
625
- await this.checkForUpdate(response.result.expectedRunnerVersion);
626
- }
627
- }
628
- catch (error) {
629
- // Heartbeat errors are already logged in sendToOrchestrator
630
- }
631
- finally {
632
- this.heartbeatInFlight = false;
633
- }
634
- }, this.config.heartbeatInterval);
635
- }
636
- async stopAllConversations(isRunnerShutdown = true) {
637
- const stopPromises = [];
638
- for (const [conversationId, context] of this.agentConversations) {
639
- if (context.status === "active" || context.status === "starting") {
640
- // Active/starting conversations: properly stop them via manager
641
- const manager = this.getManagerForContext(context);
642
- stopPromises.push(manager
643
- .stopConversation(context.agentSessionId, context, isRunnerShutdown)
644
- .catch((error) => {
645
- logger.error(`Failed to stop conversation ${context.agentSessionId}:`, error);
646
- }));
647
- }
648
- else {
649
- // Already stopped/errored conversations: ensure orchestrator is notified
650
- // This prevents tasks from restarting on runner restart
651
- stopPromises.push(this.notify("conversation.end", {
652
- conversationId: context.conversationId,
653
- conversationObjectType: context.conversationObjectType,
654
- conversationObjectId: context.conversationObjectId,
655
- agentSessionId: context.agentSessionId,
656
- isError: context.status === "error",
657
- reason: "runner_shutdown_cleanup",
658
- }).catch((error) => {
659
- logger.error(`Failed to notify conversation.end for ${conversationId}:`, error);
660
- }));
661
- }
662
- }
663
- await Promise.all(stopPromises);
664
- this.agentConversations.clear();
665
- }
666
- // Update lastProcessedAt and persist state
667
- async updateLastProcessedAt(timestamp) {
668
- this.lastProcessedAt = timestamp;
669
- if (timestamp) {
670
- await this.stateManager.updateLastProcessedAt(timestamp);
671
- }
672
- }
673
- // Getters for components
674
- get config_() {
675
- return this.config;
676
- }
677
- get activeConversations_() {
678
- return this.agentConversations;
679
- }
680
- get claudeManager_() {
681
- return this.claudeManager;
682
- }
683
- get codexManager_() {
684
- return this.codexManager;
685
- }
686
- get northflareAgentManager_() {
687
- return this.northflareAgentManager;
688
- }
689
- get repositoryManager_() {
690
- return this.repositoryManager;
691
- }
692
- getConfigPath() {
693
- return this.configPath;
694
- }
695
- getRunnerId() {
696
- return this.config.runnerId;
697
- }
698
- // UID ownership getters/setters
699
- getRunnerUid() {
700
- return this.runnerUid;
701
- }
702
- getLastProcessedAt() {
703
- return this.lastProcessedAt;
704
- }
705
- getIsActiveRunner() {
706
- return this.isActiveRunner;
707
- }
708
- setIsActiveRunner(active) {
709
- const previousState = this.isActiveRunner;
710
- this.isActiveRunner = active;
711
- // Persist state change
712
- this.stateManager.updateActiveStatus(active).catch((error) => {
713
- logger.error("Failed to persist active status:", error);
714
- });
715
- if (isRunnerDebugEnabled()) {
716
- logger.debug("Active runner status changed", {
717
- previous: previousState,
718
- new: active,
719
- runnerUid: this.runnerUid,
720
- lastProcessedAt: this.lastProcessedAt?.toISOString() || "null",
721
- });
722
- }
723
- }
724
- setLastProcessedAt(timestamp) {
725
- const previousTimestamp = this.lastProcessedAt;
726
- this.lastProcessedAt = timestamp;
727
- if (isRunnerDebugEnabled()) {
728
- logger.debug("LastProcessedAt updated", {
729
- previous: previousTimestamp?.toISOString() || "null",
730
- new: timestamp?.toISOString() || "null",
731
- runnerUid: this.runnerUid,
732
- isActiveRunner: this.isActiveRunner,
733
- });
734
- }
735
- }
736
- getPreHandoffConversations() {
737
- return this.preHandoffConversations;
738
- }
739
- // For testing purposes - allows mocking delays
740
- setDelayFunction(fn) {
741
- this.delayFn = fn;
742
- }
743
- getWorkspacePath() {
744
- return this.config.workspacePath;
745
- }
746
- /**
747
- * Check if an update is available and trigger update process if needed.
748
- * Called by message handler when a message includes expectedRunnerVersion.
749
- */
750
- async checkForUpdate(serverVersion) {
751
- await this.updateCoordinator.checkVersion(serverVersion);
752
- }
753
- /**
754
- * Notify the update coordinator that a conversation has ended.
755
- * If there's a pending update and this was the last active conversation,
756
- * the update will be triggered.
757
- */
758
- async onConversationEnd() {
759
- await this.updateCoordinator.onConversationEnd();
760
- }
761
- /**
762
- * Check if there's a pending update waiting for conversations to complete.
763
- */
764
- hasPendingUpdate() {
765
- return this.updateCoordinator.hasPendingUpdate;
766
- }
767
- async replaceRunnerRepos(repos) {
768
- this.config.runnerRepos = repos;
769
- await this.saveRunnerRepoCache(repos);
770
- }
771
- async saveRunnerRepoCache(repos) {
772
- if (!this.runnerReposCachePath)
773
- return;
774
- try {
775
- await fs.mkdir(path.dirname(this.runnerReposCachePath), { recursive: true });
776
- await fs.writeFile(this.runnerReposCachePath, JSON.stringify({ updatedAt: new Date().toISOString(), repos }, null, 2), "utf-8");
777
- }
778
- catch (err) {
779
- logger.warn("Failed to persist runner repo cache", err);
780
- }
781
- }
782
- async loadRunnerRepoCache() {
783
- if (!this.runnerReposCachePath)
784
- return null;
785
- try {
786
- const content = await fs.readFile(this.runnerReposCachePath, "utf-8");
787
- const parsed = JSON.parse(content);
788
- if (Array.isArray(parsed?.repos)) {
789
- logger.info(`Loaded ${parsed.repos.length} runner repos from cache at ${this.runnerReposCachePath}`);
790
- return parsed.repos;
791
- }
792
- }
793
- catch (err) {
794
- if (err?.code !== "ENOENT") {
795
- logger.warn("Failed to load runner repo cache", err);
796
- }
797
- }
798
- return null;
799
- }
800
- isRepoAllowed(repo, workspacePath) {
801
- const base = workspacePath || this.config.workspacePath;
802
- const resolvedPath = path.resolve(repo.path);
803
- const external = repo.external === true || !isSubPath(base, resolvedPath);
804
- if (!external && base && !isSubPath(base, resolvedPath)) {
805
- return false;
806
- }
807
- return true;
808
- }
809
- async prepareRunnerRepos(repos, workspacePath) {
810
- const prepared = [];
811
- for (const repo of repos || []) {
812
- if (!repo?.path || !repo?.name)
813
- continue;
814
- const resolvedPath = path.resolve(repo.path);
815
- const external = repo.external === true || !isSubPath(workspacePath || this.config.workspacePath, resolvedPath);
816
- if (!external && workspacePath && !isSubPath(workspacePath, resolvedPath)) {
817
- logger.warn(`Ignoring runnerRepo ${repo.name} because path is outside workspace: ${resolvedPath}`);
818
- continue;
819
- }
820
- if (!external && this.config.workspacePath && !isSubPath(this.config.workspacePath, resolvedPath)) {
821
- logger.warn(`Ignoring runnerRepo ${repo.name} because path is outside configured workspace: ${resolvedPath}`);
822
- continue;
823
- }
824
- try {
825
- await fs.mkdir(resolvedPath, { recursive: true });
826
- const stats = await fs.stat(resolvedPath);
827
- if (!stats.isDirectory()) {
828
- logger.warn(`RunnerRepo path is not a directory: ${repo.name} at ${resolvedPath}`);
829
- continue;
830
- }
831
- }
832
- catch (err) {
833
- logger.warn(`RunnerRepo path could not be prepared: ${repo.name} at ${resolvedPath}`, err);
834
- continue;
835
- }
836
- prepared.push({
837
- ...repo,
838
- path: resolvedPath,
839
- external,
840
- });
841
- }
842
- return prepared;
843
- }
844
- async refreshRunnerReposFromServer(useCacheOnFailure = true) {
845
- const apiClient = new RunnerAPIClient(this.config);
846
- if (this.config.runnerId) {
847
- apiClient.setRunnerId(this.config.runnerId);
848
- }
849
- try {
850
- const snapshot = await apiClient.listRunnerRepos();
851
- const prepared = await this.prepareRunnerRepos(snapshot.repos || [], snapshot.workspacePath);
852
- await this.replaceRunnerRepos(prepared);
853
- logger.info(`Hydrated runner repos from server snapshot (${prepared.length})`);
854
- }
855
- catch (err) {
856
- logger.error("Failed to fetch runner repo snapshot from server", err);
857
- if (useCacheOnFailure) {
858
- const cached = await this.loadRunnerRepoCache();
859
- if (cached) {
860
- const prepared = await this.prepareRunnerRepos(cached);
861
- await this.replaceRunnerRepos(prepared);
862
- logger.info(`Hydrated runner repos from local cache (${prepared.length}) after snapshot failure`);
863
- return;
864
- }
865
- }
866
- throw err;
867
- }
868
- }
869
- getManagerForContext(context) {
870
- const provider = context?.provider?.toLowerCase();
871
- if (provider === "openai") {
872
- return this.codexManager;
873
- }
874
- return this.claudeManager;
875
- }
876
- }
877
- //# sourceMappingURL=runner-sse.js.map