@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.
- package/bin/northflare-runner +1 -1
- package/dist/chunk-3QTLJ4CG.js +33622 -0
- package/dist/chunk-3QTLJ4CG.js.map +1 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/dist-W7DZRE4U.js +365 -0
- package/dist/dist-W7DZRE4U.js.map +1 -0
- package/dist/index.d.ts +764 -5
- package/dist/index.js +9872 -202
- package/dist/index.js.map +1 -1
- package/dist/sdk-query-TRMSGGID-EIENWDKW.js +14 -0
- package/dist/sdk-query-TRMSGGID-EIENWDKW.js.map +1 -0
- package/package.json +17 -17
- package/tsup.config.ts +5 -2
- package/dist/components/claude-sdk-manager.d.ts +0 -60
- package/dist/components/claude-sdk-manager.d.ts.map +0 -1
- package/dist/components/claude-sdk-manager.js +0 -1378
- package/dist/components/claude-sdk-manager.js.map +0 -1
- package/dist/components/codex-sdk-manager.d.ts +0 -94
- package/dist/components/codex-sdk-manager.d.ts.map +0 -1
- package/dist/components/codex-sdk-manager.js +0 -1450
- package/dist/components/codex-sdk-manager.js.map +0 -1
- package/dist/components/enhanced-repository-manager.d.ts +0 -173
- package/dist/components/enhanced-repository-manager.d.ts.map +0 -1
- package/dist/components/enhanced-repository-manager.js +0 -1097
- package/dist/components/enhanced-repository-manager.js.map +0 -1
- package/dist/components/message-handler-sse.d.ts +0 -77
- package/dist/components/message-handler-sse.d.ts.map +0 -1
- package/dist/components/message-handler-sse.js +0 -1224
- package/dist/components/message-handler-sse.js.map +0 -1
- package/dist/components/northflare-agent-sdk-manager.d.ts +0 -58
- package/dist/components/northflare-agent-sdk-manager.d.ts.map +0 -1
- package/dist/components/northflare-agent-sdk-manager.js +0 -2032
- package/dist/components/northflare-agent-sdk-manager.js.map +0 -1
- package/dist/components/repository-manager.d.ts +0 -51
- package/dist/components/repository-manager.d.ts.map +0 -1
- package/dist/components/repository-manager.js +0 -256
- package/dist/components/repository-manager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/runner-sse.d.ts +0 -102
- package/dist/runner-sse.d.ts.map +0 -1
- package/dist/runner-sse.js +0 -877
- package/dist/runner-sse.js.map +0 -1
- package/dist/services/RunnerAPIClient.d.ts +0 -61
- package/dist/services/RunnerAPIClient.d.ts.map +0 -1
- package/dist/services/RunnerAPIClient.js +0 -187
- package/dist/services/RunnerAPIClient.js.map +0 -1
- package/dist/services/SSEClient.d.ts +0 -62
- package/dist/services/SSEClient.d.ts.map +0 -1
- package/dist/services/SSEClient.js +0 -225
- package/dist/services/SSEClient.js.map +0 -1
- package/dist/types/claude.d.ts +0 -80
- package/dist/types/claude.d.ts.map +0 -1
- package/dist/types/claude.js +0 -5
- package/dist/types/claude.js.map +0 -1
- package/dist/types/index.d.ts +0 -52
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -7
- package/dist/types/index.js.map +0 -1
- package/dist/types/messages.d.ts +0 -33
- package/dist/types/messages.d.ts.map +0 -1
- package/dist/types/messages.js +0 -5
- package/dist/types/messages.js.map +0 -1
- package/dist/types/runner-interface.d.ts +0 -38
- package/dist/types/runner-interface.d.ts.map +0 -1
- package/dist/types/runner-interface.js +0 -5
- package/dist/types/runner-interface.js.map +0 -1
- package/dist/utils/StateManager.d.ts +0 -61
- package/dist/utils/StateManager.d.ts.map +0 -1
- package/dist/utils/StateManager.js +0 -170
- package/dist/utils/StateManager.js.map +0 -1
- package/dist/utils/config.d.ts +0 -48
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -378
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/console.d.ts +0 -8
- package/dist/utils/console.d.ts.map +0 -1
- package/dist/utils/console.js +0 -31
- package/dist/utils/console.js.map +0 -1
- package/dist/utils/debug.d.ts +0 -12
- package/dist/utils/debug.d.ts.map +0 -1
- package/dist/utils/debug.js +0 -94
- package/dist/utils/debug.js.map +0 -1
- package/dist/utils/expand-env.d.ts +0 -2
- package/dist/utils/expand-env.d.ts.map +0 -1
- package/dist/utils/expand-env.js +0 -17
- package/dist/utils/expand-env.js.map +0 -1
- package/dist/utils/inactivity-timeout.d.ts +0 -19
- package/dist/utils/inactivity-timeout.d.ts.map +0 -1
- package/dist/utils/inactivity-timeout.js +0 -72
- package/dist/utils/inactivity-timeout.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -10
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -129
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/message-log.d.ts +0 -23
- package/dist/utils/message-log.d.ts.map +0 -1
- package/dist/utils/message-log.js +0 -69
- package/dist/utils/message-log.js.map +0 -1
- package/dist/utils/model.d.ts +0 -8
- package/dist/utils/model.d.ts.map +0 -1
- package/dist/utils/model.js +0 -37
- package/dist/utils/model.js.map +0 -1
- package/dist/utils/status-line.d.ts +0 -34
- package/dist/utils/status-line.d.ts.map +0 -1
- package/dist/utils/status-line.js +0 -131
- package/dist/utils/status-line.js.map +0 -1
- package/dist/utils/tool-response-sanitizer.d.ts +0 -9
- package/dist/utils/tool-response-sanitizer.d.ts.map +0 -1
- package/dist/utils/tool-response-sanitizer.js +0 -118
- package/dist/utils/tool-response-sanitizer.js.map +0 -1
- package/dist/utils/update-coordinator.d.ts +0 -53
- package/dist/utils/update-coordinator.d.ts.map +0 -1
- package/dist/utils/update-coordinator.js +0 -159
- package/dist/utils/update-coordinator.js.map +0 -1
- package/dist/utils/version.d.ts +0 -10
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -33
- 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
|