@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,1224 +0,0 @@
1
- /**
2
- * MessageHandler - Processes incoming JSONRPC messages from SSE events
3
- */
4
- import { SSEClient } from '../services/SSEClient.js';
5
- import { RunnerAPIClient } from '../services/RunnerAPIClient.js';
6
- import { statusLineManager } from '../utils/status-line.js';
7
- import { createLogger } from '../utils/logger.js';
8
- import { isDebugEnabledFor } from '../utils/debug.js';
9
- import { buildIncomingMessageLog, messageFlowLogger, messagesDebugEnabled, } from '../utils/message-log.js';
10
- import fs from "fs/promises";
11
- import path from "path";
12
- const logger = createLogger("MessageHandler", "rpc");
13
- const sseLogger = createLogger("MessageHandler:SSE", "sse");
14
- // Use getters so debug state is checked at call time, not module load time
15
- const rpcDebugEnabled = () => isDebugEnabledFor("rpc");
16
- const sseDebugEnabled = () => isDebugEnabledFor("sse");
17
- const isSubPath = (base, candidate) => {
18
- if (!base || !candidate)
19
- return false;
20
- const normBase = path.resolve(base);
21
- const normCandidate = path.resolve(candidate);
22
- return (normCandidate === normBase ||
23
- normCandidate.startsWith(normBase.endsWith(path.sep) ? normBase : normBase + path.sep));
24
- };
25
- export class MessageHandler {
26
- methodHandlers;
27
- runner;
28
- /** Map of message ID to message timestamp for deduplication */
29
- processedMessages = new Map();
30
- /**
31
- * Queue SSE events to provide backpressure.
32
- * Without this, async handler promises can accumulate unbounded under load.
33
- */
34
- sseEventQueue = [];
35
- isDrainingSseQueue = false;
36
- isRecoveringSse = false;
37
- maxSseQueueLength;
38
- sseClient = null;
39
- apiClient;
40
- isProcessing = false;
41
- constructor(runner) {
42
- this.runner = runner;
43
- this.methodHandlers = new Map();
44
- this.apiClient = new RunnerAPIClient(runner.config_);
45
- this.maxSseQueueLength = this.readMaxSseQueueLength();
46
- this.registerHandlers();
47
- }
48
- async startProcessing() {
49
- logger.info("Starting message processing with SSE", {
50
- runnerId: this.runner.getRunnerId(),
51
- });
52
- if (this.isProcessing) {
53
- logger.warn("Message processing already started");
54
- return;
55
- }
56
- this.isProcessing = true;
57
- // Update API client with runner ID if available
58
- const runnerId = this.runner.getRunnerId();
59
- if (runnerId) {
60
- this.apiClient.setRunnerId(runnerId);
61
- }
62
- // Catch up on missed messages first
63
- await this.catchUpMissedMessages();
64
- // Start SSE connection
65
- await this.connectSSE();
66
- }
67
- async stopProcessing() {
68
- logger.info("Stopping message processing");
69
- this.isProcessing = false;
70
- // Stop SSE client
71
- if (this.sseClient) {
72
- this.sseClient.stop();
73
- this.sseClient = null;
74
- }
75
- // Clear queued events and stop drain loop
76
- this.sseEventQueue.length = 0;
77
- this.isDrainingSseQueue = false;
78
- this.isRecoveringSse = false;
79
- // NOTE: Do NOT clear processedMessages here. Keeping the map across
80
- // reconnections provides dedup protection in case the high watermark
81
- // wasn't persisted before disconnect. The pruneProcessedMessages()
82
- // method handles cleanup based on the acknowledged watermark.
83
- }
84
- /**
85
- * Prune processed messages older than the given watermark.
86
- * Messages at or before the watermark are filtered server-side,
87
- * so we don't need to track them for deduplication anymore.
88
- */
89
- pruneProcessedMessages(watermark) {
90
- const beforeSize = this.processedMessages.size;
91
- for (const [messageId, timestamp] of this.processedMessages) {
92
- if (timestamp <= watermark) {
93
- this.processedMessages.delete(messageId);
94
- }
95
- }
96
- const pruned = beforeSize - this.processedMessages.size;
97
- if (pruned > 0 && rpcDebugEnabled()) {
98
- logger.debug("Pruned processed messages", {
99
- pruned,
100
- remaining: this.processedMessages.size,
101
- watermark: watermark.toISOString(),
102
- });
103
- }
104
- }
105
- /**
106
- * Catch up on missed messages since lastProcessedAt
107
- */
108
- async catchUpMissedMessages() {
109
- const lastProcessedAt = this.runner.getLastProcessedAt();
110
- if (!lastProcessedAt) {
111
- logger.debug("No lastProcessedAt timestamp, skipping catch-up");
112
- return;
113
- }
114
- logger.info("Catching up on messages", {
115
- since: lastProcessedAt.toISOString(),
116
- });
117
- try {
118
- const messages = await this.apiClient.fetchMissedMessages({
119
- since: lastProcessedAt,
120
- limit: 1000,
121
- });
122
- if (messages.length > 0) {
123
- logger.info("Processing missed messages", { count: messages.length });
124
- // Process messages in order
125
- for (const message of messages) {
126
- await this.processMessage(message);
127
- }
128
- }
129
- }
130
- catch (error) {
131
- logger.error("Failed to catch up on missed messages:", error);
132
- // Continue anyway - SSE connection might still work
133
- }
134
- }
135
- /**
136
- * Connect to SSE endpoint
137
- */
138
- async connectSSE() {
139
- const runnerId = this.runner.getRunnerId();
140
- if (!runnerId) {
141
- throw new Error("Cannot connect to SSE without runner ID");
142
- }
143
- const token = process.env["NORTHFLARE_RUNNER_TOKEN"];
144
- if (!token) {
145
- throw new Error("Missing NORTHFLARE_RUNNER_TOKEN");
146
- }
147
- logger.info("Connecting to SSE endpoint", { runnerId });
148
- this.sseClient = new SSEClient({
149
- url: `${this.runner.config_.orchestratorUrl}/api/runner-events`,
150
- runnerId,
151
- token,
152
- // Important: onMessage must be sync to avoid accumulating unawaited promises.
153
- // Queue events and process them sequentially to provide backpressure.
154
- onMessage: this.enqueueSseEvent.bind(this),
155
- onError: (error) => {
156
- logger.error("SSE connection error:", error);
157
- },
158
- onConnect: () => {
159
- sseLogger.info("SSE connection established", { runnerId });
160
- },
161
- onDisconnect: () => {
162
- sseLogger.info("SSE connection closed", { runnerId });
163
- },
164
- reconnectInterval: 1000,
165
- maxReconnectInterval: 30000,
166
- reconnectMultiplier: 2,
167
- // Provide callback to get current lastProcessedAt for reconnection
168
- getLastProcessedAt: () => this.runner.getLastProcessedAt()?.toISOString(),
169
- });
170
- // Connect with lastProcessedAt for server-side filtering
171
- const lastProcessedAt = this.runner.getLastProcessedAt();
172
- await this.sseClient.connect(lastProcessedAt?.toISOString());
173
- }
174
- readMaxSseQueueLength() {
175
- const raw = process.env["NORTHFLARE_RUNNER_SSE_QUEUE_MAX"];
176
- if (!raw)
177
- return 5000;
178
- const parsed = Number(raw);
179
- if (!Number.isFinite(parsed) || parsed <= 0)
180
- return 5000;
181
- return Math.floor(parsed);
182
- }
183
- enqueueSseEvent(event) {
184
- if (!this.isProcessing)
185
- return;
186
- this.sseEventQueue.push(event);
187
- if (this.sseEventQueue.length > this.maxSseQueueLength) {
188
- // Apply backpressure by reconnecting and catching up from lastProcessedAt.
189
- // We intentionally clear the queue: unacknowledged messages will be
190
- // returned by catchUpMissedMessages() because lastProcessedAt hasn't advanced.
191
- sseLogger.warn("SSE event queue overflow; reconnecting to catch up", {
192
- queued: this.sseEventQueue.length,
193
- max: this.maxSseQueueLength,
194
- lastProcessedAt: this.runner.getLastProcessedAt()?.toISOString() || "null",
195
- });
196
- void this.recoverSseFromBackpressure();
197
- return;
198
- }
199
- if (!this.isDrainingSseQueue) {
200
- void this.drainSseQueue();
201
- }
202
- }
203
- async recoverSseFromBackpressure() {
204
- if (this.isRecoveringSse)
205
- return;
206
- this.isRecoveringSse = true;
207
- try {
208
- // Stop current SSE connection if any
209
- if (this.sseClient) {
210
- this.sseClient.stop();
211
- this.sseClient = null;
212
- }
213
- // Clear queue to free memory immediately
214
- this.sseEventQueue.length = 0;
215
- // Catch up via REST, then reconnect SSE
216
- await this.catchUpMissedMessages();
217
- if (this.isProcessing) {
218
- await this.connectSSE();
219
- }
220
- }
221
- catch (error) {
222
- sseLogger.error("Failed to recover SSE after backpressure", error);
223
- }
224
- finally {
225
- this.isRecoveringSse = false;
226
- }
227
- }
228
- async drainSseQueue() {
229
- if (this.isDrainingSseQueue)
230
- return;
231
- this.isDrainingSseQueue = true;
232
- try {
233
- while (this.isProcessing && this.sseEventQueue.length > 0) {
234
- const next = this.sseEventQueue.shift();
235
- if (!next)
236
- break;
237
- await this.handleSSEEvent(next);
238
- }
239
- }
240
- catch (error) {
241
- sseLogger.error("SSE queue drain failed", error);
242
- }
243
- finally {
244
- this.isDrainingSseQueue = false;
245
- }
246
- }
247
- /**
248
- * Handle SSE event
249
- */
250
- async handleSSEEvent(event) {
251
- if (event.type === "runner.message") {
252
- const message = event.data;
253
- if (sseDebugEnabled()) {
254
- sseLogger.debug("Received SSE event", {
255
- eventId: event.id,
256
- type: event.type,
257
- messageId: message?.id,
258
- method: message?.payload?.method,
259
- createdAt: message?.createdAt,
260
- });
261
- }
262
- try {
263
- // Process the message
264
- await this.processMessage(message);
265
- }
266
- catch (err) {
267
- const msg = err instanceof Error ? err.message : String(err);
268
- logger.error("Failed to process runner.message:", msg);
269
- // Do not crash the runner on a single bad message
270
- return;
271
- }
272
- }
273
- else if (event.type === "connection.established") {
274
- sseLogger.debug("SSE connection established", event.data);
275
- }
276
- else if (event.type === "runnerRepo.created" || event.type === "runnerRepo.updated") {
277
- await this.handleRunnerRepoUpsert(event.data);
278
- }
279
- else if (event.type === "runnerRepo.deleted") {
280
- await this.handleRunnerRepoDeleted(event.data);
281
- }
282
- else {
283
- sseLogger.debug(`Received event type: ${event.type}`, event.data);
284
- }
285
- }
286
- async processMessage(message) {
287
- if (rpcDebugEnabled()) {
288
- logger.debug("processMessage called", {
289
- messageId: message.id,
290
- method: message.payload?.method,
291
- direction: message.direction,
292
- createdAt: message.createdAt,
293
- });
294
- }
295
- // Check for version update signal (do this early, before any filtering)
296
- if (message.expectedRunnerVersion) {
297
- await this.runner.checkForUpdate(message.expectedRunnerVersion);
298
- }
299
- // Skip if already processed
300
- if (this.processedMessages.has(message.id)) {
301
- return;
302
- }
303
- if (messagesDebugEnabled()) {
304
- messageFlowLogger.debug("[incoming] orchestrator -> runner", buildIncomingMessageLog(message));
305
- }
306
- // Check if we should process this message based on ownership
307
- if (!this.shouldProcessMessage(message)) {
308
- return;
309
- }
310
- const { method, params } = message.payload;
311
- if (!method) {
312
- await this.sendError(message, "Missing method in message payload");
313
- return;
314
- }
315
- const handler = this.methodHandlers.get(method);
316
- if (!handler) {
317
- await this.sendError(message, `Unknown method: ${method}`);
318
- return;
319
- }
320
- if (rpcDebugEnabled()) {
321
- logger.debug("Processing message", {
322
- messageId: message.id,
323
- method: method,
324
- taskId: message.taskId,
325
- isActionMessage: this.isActionMessage(message),
326
- });
327
- }
328
- try {
329
- await handler(params, message);
330
- await this.markProcessed(message);
331
- // Acknowledge ALL messages to update lastProcessedAt
332
- await this.acknowledgeMessage(message);
333
- if (rpcDebugEnabled()) {
334
- logger.debug("Message acknowledged", {
335
- messageId: message.id,
336
- method: method,
337
- timestamp: message.createdAt,
338
- wasActionMessage: this.isActionMessage(message),
339
- });
340
- }
341
- }
342
- catch (error) {
343
- if (rpcDebugEnabled()) {
344
- logger.debug("Message processing error", {
345
- messageId: message.id,
346
- method: method,
347
- error: error instanceof Error ? error.message : String(error),
348
- });
349
- }
350
- await this.handleError(message, error);
351
- }
352
- }
353
- async handleRunnerRepoUpsert(eventData) {
354
- const runnerExternalId = this.runner.getRunnerId();
355
- if (!runnerExternalId)
356
- return;
357
- const targetRunnerExternalId = eventData?.runner?.id;
358
- if (targetRunnerExternalId && targetRunnerExternalId !== runnerExternalId) {
359
- return;
360
- }
361
- const runnerType = eventData?.runner?.type || "local";
362
- if (runnerType !== "local")
363
- return;
364
- const runnerPath = eventData?.runnerPath;
365
- const repoName = eventData?.name;
366
- const repoUuid = eventData?.uuid;
367
- const external = eventData?.external === true;
368
- if (!runnerPath || !repoName || !repoUuid)
369
- return;
370
- const workspacePath = this.runner.getWorkspacePath?.();
371
- const pathAllowed = external || (workspacePath ? isSubPath(workspacePath, runnerPath) : true);
372
- if (!pathAllowed) {
373
- logger.warn(`Ignoring runnerRepo upsert outside workspace from SSE: ${runnerPath}`);
374
- return;
375
- }
376
- try {
377
- await fs.mkdir(runnerPath, { recursive: true });
378
- logger.info(`Ensured local workspace directory exists at ${runnerPath}`);
379
- }
380
- catch (error) {
381
- logger.error("Failed to create workspace directory", error);
382
- }
383
- const repos = Array.isArray(this.runner.config_.runnerRepos)
384
- ? [...this.runner.config_.runnerRepos]
385
- : [];
386
- const existingIndex = repos.findIndex((r) => r.uuid === repoUuid || r.path === runnerPath || r.name === repoName);
387
- const updatedEntry = {
388
- uuid: repoUuid,
389
- name: repoName,
390
- path: runnerPath,
391
- external,
392
- };
393
- if (existingIndex >= 0) {
394
- repos[existingIndex] = updatedEntry;
395
- }
396
- else {
397
- repos.push(updatedEntry);
398
- }
399
- // Persist to runner cache if available
400
- if (typeof this.runner.replaceRunnerRepos === "function") {
401
- await this.runner.replaceRunnerRepos(repos);
402
- }
403
- else {
404
- this.runner.config_.runnerRepos = repos;
405
- }
406
- logger.info(`Updated in-memory runnerRepo ${repoName} from SSE event`);
407
- }
408
- async handleRunnerRepoDeleted(eventData) {
409
- const runnerExternalId = this.runner.getRunnerId();
410
- if (!runnerExternalId)
411
- return;
412
- const targetRunnerExternalId = eventData?.runner?.id;
413
- if (targetRunnerExternalId && targetRunnerExternalId !== runnerExternalId) {
414
- return;
415
- }
416
- const runnerType = eventData?.runner?.type || "local";
417
- if (runnerType !== "local")
418
- return;
419
- const repoUuid = eventData?.uuid;
420
- const repoName = eventData?.name;
421
- if (!repoUuid && !repoName)
422
- return;
423
- const repos = Array.isArray(this.runner.config_.runnerRepos)
424
- ? [...this.runner.config_.runnerRepos]
425
- : [];
426
- const filtered = repos.filter((repo) => (repoUuid ? repo.uuid !== repoUuid : true) &&
427
- (repoName ? repo.name !== repoName : true));
428
- if (filtered.length === repos.length) {
429
- return;
430
- }
431
- if (typeof this.runner.replaceRunnerRepos === "function") {
432
- await this.runner.replaceRunnerRepos(filtered);
433
- }
434
- else {
435
- this.runner.config_.runnerRepos = filtered;
436
- }
437
- logger.info(`Removed runnerRepo ${repoName || repoUuid} from in-memory state after deletion event`);
438
- }
439
- shouldProcessMessage(message) {
440
- const decision = (() => {
441
- // Always process our own responses going to orchestrator
442
- if (message.direction === "to_orchestrator") {
443
- return { shouldProcess: true, reason: "own response to orchestrator" };
444
- }
445
- // Always process UID change messages BEFORE checking timestamp
446
- // This is critical because UID changes can update lastProcessedAt itself
447
- if (message.payload?.method === "runner.uid.changed") {
448
- return { shouldProcess: true, reason: "UID change message" };
449
- }
450
- // Filter by lastProcessedAt (after checking for UID change messages)
451
- const lastProcessedAt = this.runner.getLastProcessedAt();
452
- if (lastProcessedAt && message.createdAt) {
453
- const messageTime = new Date(message.createdAt);
454
- if (messageTime <= lastProcessedAt) {
455
- return {
456
- shouldProcess: false,
457
- reason: "message before lastProcessedAt",
458
- };
459
- }
460
- }
461
- // If we're not the active runner
462
- if (!this.runner.getIsActiveRunner()) {
463
- // Only process if it's for a pre-handoff conversation by conversationId
464
- const cid = message.conversationId || message.payload?.params?.conversationId;
465
- if (cid && this.runner.getPreHandoffConversations().has(cid)) {
466
- return {
467
- shouldProcess: true,
468
- reason: "pre-handoff conversation (by conversationId)",
469
- };
470
- }
471
- return { shouldProcess: false, reason: "not active runner" };
472
- }
473
- // We're active and message is after lastProcessedAt
474
- return {
475
- shouldProcess: true,
476
- reason: "active runner, message after watermark",
477
- };
478
- })();
479
- if (rpcDebugEnabled()) {
480
- logger.debug("Message processing decision", {
481
- messageId: message.id,
482
- method: message.payload?.method,
483
- shouldProcess: decision.shouldProcess,
484
- reason: decision.reason,
485
- runnerUid: this.runner.getRunnerUid(),
486
- isActiveRunner: this.runner.getIsActiveRunner(),
487
- lastProcessedAt: this.runner.getLastProcessedAt()?.toISOString() || "null",
488
- messageCreatedAt: message.createdAt,
489
- });
490
- }
491
- return decision.shouldProcess;
492
- }
493
- isActionMessage(message) {
494
- const actionMethods = [
495
- "conversation.start",
496
- "conversation.stop",
497
- "conversation.resume",
498
- "conversation.config",
499
- "conversation.summary",
500
- "message.user",
501
- ];
502
- return actionMethods.includes(message.payload?.method || "");
503
- }
504
- async acknowledgeMessage(message) {
505
- const runnerId = this.runner.getRunnerId();
506
- if (rpcDebugEnabled()) {
507
- logger.debug("Sending message.acknowledge", {
508
- runnerId,
509
- messageTimestamp: message.createdAt,
510
- messageId: message.id,
511
- method: message.payload?.method,
512
- });
513
- }
514
- try {
515
- await this.apiClient.acknowledgeMessage(message.createdAt);
516
- // Update local lastProcessedAt
517
- const newWatermark = new Date(message.createdAt);
518
- await this.runner.updateLastProcessedAt(newWatermark);
519
- // Prune old entries from processedMessages map to prevent unbounded growth.
520
- // Messages at or before the watermark are filtered server-side, so we
521
- // don't need to track them for deduplication anymore.
522
- this.pruneProcessedMessages(newWatermark);
523
- if (rpcDebugEnabled()) {
524
- logger.debug("message.acknowledge sent", {
525
- runnerId,
526
- messageId: message.id,
527
- });
528
- }
529
- }
530
- catch (error) {
531
- logger.error("Failed to acknowledge message:", error);
532
- // Continue processing even if acknowledgment fails
533
- }
534
- }
535
- async markProcessed(message) {
536
- // Track processed messages with their timestamp for later pruning
537
- const timestamp = message.createdAt ? new Date(message.createdAt) : new Date();
538
- this.processedMessages.set(message.id, timestamp);
539
- this.pruneProcessedMessagesFailsafe();
540
- }
541
- /**
542
- * Failsafe pruning to prevent unbounded growth if acknowledgements fail
543
- * and lastProcessedAt can't advance (so watermark-based pruning can't run).
544
- */
545
- pruneProcessedMessagesFailsafe() {
546
- const MAX_ENTRIES = 20_000;
547
- const MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
548
- const now = Date.now();
549
- const cutoff = now - MAX_AGE_MS;
550
- // Fast path: nothing to do
551
- if (this.processedMessages.size <= MAX_ENTRIES) {
552
- // Still prune by age if we can do it cheaply from the head
553
- const first = this.processedMessages.entries().next().value;
554
- if (!first)
555
- return;
556
- if (first[1].getTime() >= cutoff)
557
- return;
558
- }
559
- // Prune oldest entries by age first (Map preserves insertion order)
560
- while (this.processedMessages.size > 0) {
561
- const first = this.processedMessages.entries().next().value;
562
- if (!first)
563
- break;
564
- const [, ts] = first;
565
- if (ts.getTime() >= cutoff)
566
- break;
567
- this.processedMessages.delete(first[0]);
568
- }
569
- // Enforce max size (delete oldest until within bound)
570
- while (this.processedMessages.size > MAX_ENTRIES) {
571
- const firstKey = this.processedMessages.keys().next().value;
572
- if (!firstKey)
573
- break;
574
- this.processedMessages.delete(firstKey);
575
- }
576
- }
577
- async sendError(message, errorMessage) {
578
- // Send error report with conversation object info if available
579
- const conversationObjectType = message.conversationObjectType || (message.taskId ? "Task" : undefined);
580
- const conversationObjectId = message.conversationObjectId || message.taskId;
581
- if (!conversationObjectId) {
582
- logger.warn("Cannot send error report - missing conversationObjectId", {
583
- messageId: message.id,
584
- method: message.payload?.method,
585
- error: errorMessage,
586
- runnerId: message.runnerId,
587
- });
588
- return;
589
- }
590
- await this.runner.notify("error.report", {
591
- conversationObjectType,
592
- conversationObjectId,
593
- messageId: message.id,
594
- errorType: "method_error",
595
- message: errorMessage,
596
- details: {
597
- originalMessage: message,
598
- timestamp: new Date(),
599
- },
600
- });
601
- }
602
- async handleError(message, error) {
603
- const errorMessage = error instanceof Error ? error.message : String(error);
604
- const errorStack = error instanceof Error ? error.stack : undefined;
605
- // Send error report with conversation object info if available
606
- const conversationObjectType = message.conversationObjectType || (message.taskId ? "Task" : undefined);
607
- const conversationObjectId = message.conversationObjectId || message.taskId;
608
- if (!conversationObjectId) {
609
- logger.warn("Cannot send processing error report - missing conversationObjectId", {
610
- messageId: message.id,
611
- method: message.payload?.method,
612
- error: errorMessage,
613
- stack: errorStack,
614
- runnerId: message.runnerId,
615
- });
616
- return;
617
- }
618
- await this.runner.notify("error.report", {
619
- conversationObjectType,
620
- conversationObjectId,
621
- messageId: message.id,
622
- errorType: "processing_error",
623
- message: errorMessage,
624
- details: {
625
- stack: errorStack,
626
- originalMessage: message,
627
- timestamp: new Date(),
628
- },
629
- });
630
- }
631
- registerHandlers() {
632
- logger.info("Registering message handlers");
633
- this.methodHandlers = new Map([
634
- ["conversation.start", this.handleConversationStart.bind(this)],
635
- ["conversation.stop", this.handleConversationStop.bind(this)],
636
- ["conversation.resume", this.handleConversationResume.bind(this)],
637
- ["conversation.config", this.handleConversationConfig.bind(this)],
638
- ["conversation.summary", this.handleConversationSummary.bind(this)],
639
- ["message.user", this.handleUserMessage.bind(this)],
640
- ["runner.uid.changed", this.handleUidChanged.bind(this)],
641
- ["git.operation", this.handleGitOperation.bind(this)],
642
- ["git.cleanup", this.handleGitCleanup.bind(this)],
643
- ]);
644
- }
645
- // All the handler methods remain the same as in the original file
646
- // Just copy them from the original message-handler.ts starting from line 386
647
- async handleConversationStart(params, message) {
648
- const { conversationObjectType = message.conversationObjectType || "Task", conversationObjectId = message.conversationObjectId ||
649
- params.conversation?.objectId, config, initialMessages, conversation, } = params;
650
- // Validate required parameters
651
- if (!config) {
652
- throw new Error("Missing required parameter: config");
653
- }
654
- // Require a conversation object
655
- const conversationData = conversation;
656
- if (!conversationData) {
657
- throw new Error("Missing required parameter: conversation object must be provided");
658
- }
659
- const finalObjectType = conversationObjectType || conversationData.objectType;
660
- const finalObjectId = conversationObjectId || conversationData.objectId;
661
- if (!finalObjectId) {
662
- throw new Error("Missing conversationObjectId");
663
- }
664
- if (rpcDebugEnabled()) {
665
- // Debug log the config
666
- logger.debug("conversation.start config", {
667
- conversationObjectId,
668
- hasConfig: !!config,
669
- hasRepository: !!config?.repository,
670
- repositoryType: config?.repository?.type,
671
- repositoryUrl: config?.repository?.url,
672
- workspaceId: config?.workspaceId,
673
- fullConfig: JSON.stringify(config, null, 2),
674
- });
675
- logger.debug("Received conversation instructions", {
676
- conversationId: conversationData.id,
677
- hasWorkspaceInstructions: !!conversationData.workspaceInstructions,
678
- workspaceInstructionsLength: conversationData.workspaceInstructions?.length ?? 0,
679
- workspaceInstructionsPreview: conversationData.workspaceInstructions?.slice(0, 100),
680
- hasGlobalInstructions: !!conversationData.globalInstructions,
681
- globalInstructionsLength: conversationData.globalInstructions?.length ?? 0,
682
- });
683
- }
684
- // Check if conversation is already active (prevent duplicate starts on catch-up)
685
- const conversationId = conversationData.id;
686
- if (this.runner.activeConversations_.has(conversationId)) {
687
- logger.info("Conversation already active, skipping duplicate start", {
688
- conversationId,
689
- });
690
- return;
691
- }
692
- // Check if this conversation was recently completed (prevent restart on catch-up)
693
- if (this.runner.wasConversationCompleted(conversationId)) {
694
- logger.info("Conversation already completed recently, skipping restart", { conversationId });
695
- return;
696
- }
697
- const provider = this.resolveAgentProvider(conversationData, config);
698
- const manager = this.getManagerForProvider(provider);
699
- // Start the conversation with the provided/loaded conversation details
700
- // For CodexManager (openai) we pass the provider to distinguish
701
- if (provider === "openai") {
702
- await manager.startConversation(finalObjectType, finalObjectId, config, initialMessages || [], conversationData, provider);
703
- }
704
- else {
705
- await manager.startConversation(finalObjectType, finalObjectId, config, initialMessages || [], conversationData, provider);
706
- }
707
- // Update status line
708
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
709
- }
710
- async handleConversationStop(params, message) {
711
- // Require conversationId (present at message level); do not fall back to agentSessionId/taskId
712
- const { conversationId = message.conversationId, reason } = params;
713
- if (rpcDebugEnabled()) {
714
- logger.debug("handleConversationStop invoked", {
715
- conversationId,
716
- messageConversationId: message.conversationId,
717
- agentSessionId: params?.agentSessionId,
718
- taskId: params?.taskId,
719
- params: JSON.stringify(params),
720
- activeConversations: this.runner.activeConversations_.size,
721
- conversationIds: Array.from(this.runner.activeConversations_.keys()),
722
- });
723
- }
724
- // Lookup strictly by conversationId
725
- let context;
726
- let targetConversationId;
727
- if (conversationId) {
728
- context = this.runner.getConversationContext(conversationId);
729
- targetConversationId = conversationId;
730
- }
731
- if (rpcDebugEnabled()) {
732
- logger.debug("handleConversationStop lookup", {
733
- contextFound: !!context,
734
- targetConversationId,
735
- contextTaskId: context?.taskId,
736
- contextAgentSessionId: context?.agentSessionId,
737
- });
738
- }
739
- // Check if we have any identifier to work with
740
- if (!conversationId) {
741
- throw new Error("Missing required parameter: conversationId");
742
- }
743
- if (context && targetConversationId) {
744
- context.status = "stopping";
745
- const manager = this.getManagerForConversationContext(context);
746
- await manager.stopConversation(context.agentSessionId, context, false, // Not a runner shutdown
747
- reason // Pass the reason through
748
- );
749
- context.status = "stopped";
750
- this.runner.activeConversations_.delete(targetConversationId);
751
- // Update status line
752
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
753
- }
754
- else {
755
- // No conversation found - this is expected as conversations may have already ended
756
- // or been cleaned up. Just log it and update status line.
757
- logger.info("Conversation stop requested but not found", {
758
- conversationId,
759
- });
760
- // If we have a targetConversationId, ensure it's removed from tracking
761
- if (targetConversationId) {
762
- this.runner.activeConversations_.delete(targetConversationId);
763
- }
764
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
765
- }
766
- }
767
- async handleConversationResume(params, message) {
768
- const { conversationId = message.conversationId, conversation: resumeConversation, config, message: resumeMessage, } = params;
769
- const cid = params.conversationId || conversationId;
770
- if (!cid) {
771
- throw new Error("Missing required parameter: conversationId");
772
- }
773
- // Require conversation details
774
- const conversationData = resumeConversation;
775
- if (!conversationData) {
776
- throw new Error("Missing required parameter: conversation object must be provided");
777
- }
778
- const agentSessionId = conversationData.agentSessionId;
779
- if (!agentSessionId) {
780
- throw new Error("Cannot resume conversation without agentSessionId");
781
- }
782
- const provider = this.resolveAgentProvider(conversationData, config);
783
- const manager = this.getManagerForProvider(provider);
784
- // For CodexManager (openai) we pass the provider to distinguish
785
- if (provider === "openai") {
786
- await manager.resumeConversation(conversationData.objectType, conversationData.objectId, agentSessionId, config, conversationData, resumeMessage, provider);
787
- }
788
- else {
789
- await manager.resumeConversation(conversationData.objectType, conversationData.objectId, agentSessionId, config, conversationData, resumeMessage, provider);
790
- }
791
- }
792
- async handleConversationConfig(params, message) {
793
- const { conversationId, model, permissionsMode, config } = params;
794
- if (rpcDebugEnabled()) {
795
- logger.debug("handleConversationConfig invoked", {
796
- conversationId,
797
- model,
798
- permissionsMode,
799
- hasConfig: !!config,
800
- });
801
- }
802
- // Find the active conversation by conversationId only
803
- let context;
804
- if (conversationId) {
805
- context = this.runner.getConversationContext(conversationId);
806
- }
807
- if (!context) {
808
- throw new Error(`No active conversation found for conversationId: ${conversationId}`);
809
- }
810
- // Validate required parameters
811
- if (!model && !permissionsMode) {
812
- throw new Error("At least one of model or permissionsMode must be provided");
813
- }
814
- // Update the config with new values
815
- const newConfig = {
816
- ...context.config,
817
- ...(model && { model }),
818
- ...(permissionsMode && { permissionsMode }),
819
- ...config, // Allow full config overrides if provided
820
- };
821
- logger.info("Stopping conversation to apply new config", {
822
- conversationId: context.conversationId,
823
- });
824
- // Stop the current conversation
825
- const manager = this.getManagerForConversationContext(context);
826
- await manager.stopConversation(context.agentSessionId, context, false // Not a runner shutdown, just updating config
827
- );
828
- // Remove from active conversations
829
- this.runner.activeConversations_.delete(context.conversationId);
830
- logger.info("Resuming conversation with updated config", {
831
- conversationId: context.conversationId,
832
- });
833
- // Resume with new config
834
- // For CodexManager (openai) we pass the provider to distinguish
835
- const contextProvider = context.provider?.toLowerCase();
836
- if (contextProvider === "openai") {
837
- await manager.resumeConversation(context.conversationObjectType, context.conversationObjectId, context.agentSessionId, newConfig, {
838
- id: context.conversationId,
839
- objectType: context.conversationObjectType,
840
- objectId: context.conversationObjectId,
841
- model: newConfig.model,
842
- globalInstructions: context.globalInstructions,
843
- workspaceInstructions: context.workspaceInstructions,
844
- permissionsMode: newConfig.permissionsMode,
845
- agentSessionId: context.agentSessionId,
846
- }, "<system-instructions>Configuration updated. Please continue with the new settings.</system-instructions>", contextProvider);
847
- }
848
- else {
849
- await manager.resumeConversation(context.conversationObjectType, context.conversationObjectId, context.agentSessionId, newConfig, {
850
- id: context.conversationId,
851
- objectType: context.conversationObjectType,
852
- objectId: context.conversationObjectId,
853
- model: newConfig.model,
854
- globalInstructions: context.globalInstructions,
855
- workspaceInstructions: context.workspaceInstructions,
856
- permissionsMode: newConfig.permissionsMode,
857
- agentSessionId: context.agentSessionId,
858
- }, "<system-instructions>Configuration updated. Please continue with the new settings.</system-instructions>");
859
- }
860
- // Update status line
861
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
862
- logger.info("Conversation config updated", {
863
- conversationId: context.conversationId,
864
- });
865
- }
866
- async handleConversationSummary(params, _message) {
867
- const { conversationId, summary } = params;
868
- if (!conversationId) {
869
- throw new Error("Missing required parameter: conversationId");
870
- }
871
- // Allow null to mean "no summary available yet"
872
- if (summary != null && typeof summary !== "string") {
873
- throw new Error("Invalid parameter: summary must be a string or null");
874
- }
875
- const normalized = typeof summary === "string" ? summary.replace(/\s+/g, " ").trim() : "";
876
- if (rpcDebugEnabled()) {
877
- logger.debug("handleConversationSummary invoked", {
878
- conversationId,
879
- hasSummary: Boolean(normalized),
880
- summaryLength: normalized.length,
881
- });
882
- }
883
- this.runner.applyConversationSummary(conversationId, normalized || null);
884
- }
885
- async handleUserMessage(params, message) {
886
- const { conversationId, content, config, conversationObjectType = message.conversationObjectType || "Task", conversationObjectId = message.conversationObjectId, conversation, agentSessionId, } = params;
887
- // Validate required parameters
888
- if (!conversationId) {
889
- throw new Error("Missing required parameter: conversationId");
890
- }
891
- if (!content) {
892
- throw new Error("Missing required parameter: content");
893
- }
894
- let context = this.runner.getConversationContext(conversationId);
895
- let provider;
896
- let manager;
897
- if (context) {
898
- provider = context.provider;
899
- if (!provider) {
900
- throw new Error(`Conversation context missing provider for conversationId ${conversationId}. Orchestrator must set providerType/agentProviderType or prefix the model when starting the conversation.`);
901
- }
902
- manager = this.getManagerForProvider(provider);
903
- }
904
- else {
905
- // Conversation is not active anymore (likely completed/cleaned up). We can
906
- // only restart it if the orchestrator provided full conversation details
907
- // and a resolvable provider.
908
- if (!conversation) {
909
- throw new Error(`No active conversation found for ${conversationId}, and no conversation details provided to restart it.`);
910
- }
911
- provider = this.resolveAgentProvider(conversation, config);
912
- manager = this.getManagerForProvider(provider);
913
- const targetObjectId = conversationObjectId ||
914
- conversation.conversationObjectId ||
915
- conversation.objectId;
916
- if (!targetObjectId) {
917
- throw new Error(`Missing conversationObjectId for ${conversationId}; cannot start conversation.`);
918
- }
919
- // Start a fresh conversation so the SDK process exists before streaming
920
- await manager.startConversation(conversationObjectType, targetObjectId, config, [], // no initial messages here; this is a follow-up turn
921
- conversation, provider);
922
- context = this.runner.getConversationContext(conversationId);
923
- if (!context) {
924
- throw new Error(`Failed to start conversation for ${conversationId}; no context created.`);
925
- }
926
- }
927
- // For CodexManager (openai), pass the provider
928
- if (provider === "openai") {
929
- await manager.sendUserMessage(conversationId, content, config, conversationObjectType, conversationObjectId, conversation, agentSessionId, provider);
930
- }
931
- else {
932
- await manager.sendUserMessage(conversationId, content, config, conversationObjectType, conversationObjectId, conversation, agentSessionId);
933
- }
934
- }
935
- async handleUidChanged(params, _message) {
936
- const { runnerUid, lastProcessedAt } = params;
937
- logger.info("Handling UID change notification", {
938
- runnerUid,
939
- lastProcessedAt,
940
- });
941
- if (rpcDebugEnabled()) {
942
- logger.debug("UID change notification received", {
943
- newUid: runnerUid,
944
- currentUid: this.runner.getRunnerUid(),
945
- newLastProcessedAt: lastProcessedAt,
946
- currentLastProcessedAt: this.runner.getLastProcessedAt()?.toISOString() || "null",
947
- wasActiveRunner: this.runner.getIsActiveRunner(),
948
- activeConversations: this.runner.activeConversations_.size,
949
- });
950
- }
951
- // Check if the UID matches ours FIRST, before checking timestamps
952
- // This ensures we activate even if we received the message during catch-up
953
- if (runnerUid === this.runner.getRunnerUid()) {
954
- // This is our UID - we're the active runner
955
- logger.info("Runner activated as primary", {
956
- runnerUid,
957
- });
958
- this.runner.setIsActiveRunner(true);
959
- await this.runner.updateLastProcessedAt(lastProcessedAt ? new Date(lastProcessedAt) : null);
960
- if (rpcDebugEnabled()) {
961
- logger.debug("Runner activated as primary", {
962
- runnerUid: runnerUid,
963
- lastProcessedAt: lastProcessedAt,
964
- orchestratorUrl: this.runner.config_.orchestratorUrl,
965
- });
966
- }
967
- // Emit activation notification - wrap in try/catch to handle failures gracefully
968
- try {
969
- await this.runner.notify("runner.activate", {
970
- runnerId: this.runner.getRunnerId(),
971
- runnerUid: runnerUid,
972
- });
973
- }
974
- catch (error) {
975
- logger.error("Failed to send activation notification", error);
976
- if (rpcDebugEnabled()) {
977
- logger.debug("Activation notification failed", {
978
- error: error instanceof Error ? error.message : String(error),
979
- });
980
- }
981
- }
982
- // Start processing messages from the lastProcessedAt point
983
- logger.info("Activation complete; setting message watermark", {
984
- after: lastProcessedAt || "beginning",
985
- });
986
- }
987
- else {
988
- // Different UID - check if this is an old UID change that we should ignore
989
- const currentLastProcessedAt = this.runner.getLastProcessedAt();
990
- if (currentLastProcessedAt && lastProcessedAt) {
991
- const newTime = new Date(lastProcessedAt);
992
- if (newTime < currentLastProcessedAt) {
993
- logger.info("Ignoring old UID change", {
994
- lastProcessedAt,
995
- currentLastProcessedAt: currentLastProcessedAt.toISOString(),
996
- });
997
- if (rpcDebugEnabled()) {
998
- logger.debug("Ignoring old UID change", {
999
- newUid: runnerUid,
1000
- newLastProcessedAt: lastProcessedAt,
1001
- currentLastProcessedAt: currentLastProcessedAt.toISOString(),
1002
- });
1003
- }
1004
- return;
1005
- }
1006
- }
1007
- // Different UID - we're being replaced
1008
- logger.info("Runner deactivated; replacement detected", { runnerUid });
1009
- this.runner.setIsActiveRunner(false);
1010
- // Remember which conversations were active before handoff (by conversationId)
1011
- for (const [conversationId, context] of this.runner
1012
- .activeConversations_) {
1013
- if (context.status === "active") {
1014
- this.runner.getPreHandoffConversations().add(conversationId);
1015
- }
1016
- }
1017
- if (rpcDebugEnabled()) {
1018
- logger.debug("Runner deactivated - being replaced", {
1019
- newRunnerUid: runnerUid,
1020
- ourUid: this.runner.getRunnerUid(),
1021
- preHandoffConversations: Array.from(this.runner.getPreHandoffConversations()),
1022
- activeConversationsCount: this.runner.getPreHandoffConversations().size,
1023
- });
1024
- }
1025
- logger.info("Runner replacement: completing active conversations", {
1026
- activeConversationCount: this.runner.getPreHandoffConversations().size,
1027
- });
1028
- // Emit deactivation notification - wrap in try/catch to handle failures gracefully
1029
- try {
1030
- await this.runner.notify("runner.deactivate", {
1031
- runnerId: this.runner.getRunnerId(),
1032
- runnerUid: this.runner.getRunnerUid(),
1033
- activeConversations: this.runner.getPreHandoffConversations().size,
1034
- });
1035
- }
1036
- catch (error) {
1037
- logger.error("Failed to send deactivation notification", error);
1038
- if (rpcDebugEnabled()) {
1039
- logger.debug("Deactivation notification failed", {
1040
- error: error instanceof Error ? error.message : String(error),
1041
- });
1042
- }
1043
- }
1044
- }
1045
- }
1046
- async handleGitOperation(params, message) {
1047
- const { taskId, operation, params: opParams } = params;
1048
- if (!taskId || !operation) {
1049
- throw new Error("Missing required parameters: taskId and operation");
1050
- }
1051
- logger.info("Handling Git operation", { operation, taskId });
1052
- const repoManager = this.runner.repositoryManager_;
1053
- try {
1054
- switch (operation) {
1055
- case "stage":
1056
- await repoManager.stageAll(taskId);
1057
- await this.sendGitStateUpdate(taskId);
1058
- break;
1059
- case "commit":
1060
- if (!opParams?.message) {
1061
- throw new Error("Commit message is required");
1062
- }
1063
- const commitHash = await repoManager.commit(taskId, opParams.message, opParams.author);
1064
- await this.recordGitOperation(taskId, "commit", "succeeded", {
1065
- commitHash,
1066
- });
1067
- await this.sendGitStateUpdate(taskId);
1068
- break;
1069
- case "push":
1070
- // TODO: Implement push operation
1071
- logger.warn("Git push operation not yet implemented", { taskId });
1072
- break;
1073
- case "rebase":
1074
- if (!opParams?.targetBranch) {
1075
- throw new Error("Target branch is required for rebase");
1076
- }
1077
- const rebaseResult = await repoManager.rebaseTask(taskId, opParams.targetBranch);
1078
- await this.recordGitOperation(taskId, "rebase", rebaseResult.success ? "succeeded" : "failed", rebaseResult);
1079
- await this.sendGitStateUpdate(taskId);
1080
- break;
1081
- case "merge":
1082
- if (!opParams?.targetBranch) {
1083
- throw new Error("Target branch is required for merge");
1084
- }
1085
- const mergeResult = await repoManager.mergeTask(taskId, opParams.targetBranch, opParams.mode);
1086
- await this.recordGitOperation(taskId, "merge", mergeResult.success ? "succeeded" : "failed", mergeResult);
1087
- await this.sendGitStateUpdate(taskId);
1088
- break;
1089
- default:
1090
- throw new Error(`Unknown Git operation: ${operation}`);
1091
- }
1092
- }
1093
- catch (error) {
1094
- logger.error(`Git operation ${operation} failed`, {
1095
- taskId,
1096
- error,
1097
- });
1098
- await this.recordGitOperation(taskId, operation, "failed", {
1099
- error: error instanceof Error ? error.message : String(error),
1100
- });
1101
- throw error;
1102
- }
1103
- }
1104
- async handleGitCleanup(params, message) {
1105
- const { taskId, preserveBranch = false } = params;
1106
- if (!taskId) {
1107
- throw new Error("Missing required parameter: taskId");
1108
- }
1109
- logger.info("Cleaning up Git worktree", { taskId });
1110
- const repoManager = this.runner.repositoryManager_;
1111
- try {
1112
- await repoManager.removeTaskWorktree(taskId, { preserveBranch });
1113
- logger.info("Git worktree cleaned up", { taskId, preserveBranch });
1114
- }
1115
- catch (error) {
1116
- logger.error(`Failed to clean up worktree for task ${taskId}`, error);
1117
- throw error;
1118
- }
1119
- }
1120
- async sendGitStateUpdate(taskId) {
1121
- const repoManager = this.runner.repositoryManager_;
1122
- const gitState = await repoManager.getTaskState(taskId);
1123
- if (!gitState) {
1124
- logger.warn(`No Git state found for task ${taskId}`);
1125
- return;
1126
- }
1127
- // Send state update to orchestrator
1128
- await this.runner.notify("git.state.update", {
1129
- taskId,
1130
- state: {
1131
- branch: gitState.branch,
1132
- commit: gitState.lastCommit || "",
1133
- isDirty: false, // TODO: Check actual dirty state
1134
- ahead: 0, // TODO: Calculate ahead/behind
1135
- behind: 0,
1136
- },
1137
- });
1138
- }
1139
- async recordGitOperation(taskId, operation, status, details) {
1140
- // Send operation record to orchestrator
1141
- await this.runner.notify("git.operation.record", {
1142
- taskId,
1143
- operation,
1144
- status,
1145
- details,
1146
- timestamp: new Date(),
1147
- });
1148
- }
1149
- resolveAgentProvider(conversation, config) {
1150
- const model = conversation?.model ||
1151
- config?.model ||
1152
- config?.defaultModel ||
1153
- "";
1154
- const normalizedModel = model.toLowerCase();
1155
- // 1) Trust explicit provider from orchestrator/config first
1156
- const explicitProvider = conversation?.providerType ||
1157
- conversation?.agentProviderType ||
1158
- config?.providerType ||
1159
- config?.agentProviderType;
1160
- const normalizeProvider = (provider) => {
1161
- if (!provider || typeof provider !== "string")
1162
- return null;
1163
- const normalized = provider.toLowerCase();
1164
- if (normalized === "codex")
1165
- return "openai"; // codex alias for OpenAI
1166
- if (normalized === "openai")
1167
- return "openai";
1168
- if (normalized === "openrouter")
1169
- return "openrouter";
1170
- if (normalized === "groq")
1171
- return "groq";
1172
- if (normalized === "claude")
1173
- return "claude";
1174
- return null;
1175
- };
1176
- const explicit = normalizeProvider(explicitProvider);
1177
- if (explicit) {
1178
- return explicit;
1179
- }
1180
- // 2) Fallback to model prefix hints (non-fragile)
1181
- if (normalizedModel.startsWith("openrouter:") || normalizedModel.startsWith("openrouter/")) {
1182
- return "openrouter";
1183
- }
1184
- if (normalizedModel.startsWith("groq:") || normalizedModel.startsWith("groq/")) {
1185
- return "groq";
1186
- }
1187
- if (normalizedModel.startsWith("openai:") || normalizedModel.startsWith("openai/")) {
1188
- return "openai";
1189
- }
1190
- if (normalizedModel.startsWith("claude:") || normalizedModel.startsWith("claude/")) {
1191
- return "claude";
1192
- }
1193
- // 3) No explicit provider and no prefix – treat as configuration error
1194
- throw new Error("Agent provider is required. Orchestrator must set providerType/agentProviderType or prefix the model (openai:/openrouter:/groq:/claude:).");
1195
- }
1196
- getManagerForProvider(provider) {
1197
- const manager = this.resolveProviderManager(provider?.toLowerCase());
1198
- if (manager) {
1199
- return manager;
1200
- }
1201
- throw new Error(`Unknown agent provider: ${provider || "<unset>"}`);
1202
- }
1203
- getManagerForConversationContext(context) {
1204
- const manager = this.resolveProviderManager(context.provider?.toLowerCase());
1205
- if (manager) {
1206
- return manager;
1207
- }
1208
- throw new Error(`Unknown or missing agent provider on conversation context: ${context.provider || "<unset>"}`);
1209
- }
1210
- resolveProviderManager(provider) {
1211
- if (provider === "openrouter" || provider === "groq") {
1212
- // OpenRouter now handled by Northflare Agent manager
1213
- return this.runner.northflareAgentManager_;
1214
- }
1215
- if (provider === "openai" || provider === "codex") {
1216
- return this.runner.codexManager_;
1217
- }
1218
- if (provider === "claude") {
1219
- return this.runner.claudeManager_;
1220
- }
1221
- return null;
1222
- }
1223
- }
1224
- //# sourceMappingURL=message-handler-sse.js.map