@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,1450 +0,0 @@
1
- import { statusLineManager } from '../utils/status-line.js';
2
- import { createScopedConsole } from '../utils/console.js';
3
- import { expandEnv } from '../utils/expand-env.js';
4
- import { mapReasoningEffortForCodex, parseModelValue } from '../utils/model.js';
5
- import jwt from "jsonwebtoken";
6
- import fs from "fs/promises";
7
- import path from "path";
8
- import { isDebugEnabledFor } from '../utils/debug.js';
9
- import { tmpdir } from "os";
10
- import crypto from "crypto";
11
- import { simpleGit } from "simple-git";
12
- const console = createScopedConsole(isDebugEnabledFor("manager") ? "manager" : "sdk");
13
- function buildSystemPromptWithNorthflare(basePrompt, northflarePrompt) {
14
- const parts = [northflarePrompt, basePrompt]
15
- .filter((p) => typeof p === "string" && p.trim().length > 0)
16
- .map((p) => p.trim());
17
- if (parts.length === 0)
18
- return undefined;
19
- return parts.join("\n\n");
20
- }
21
- let codexSdkPromise = null;
22
- async function loadCodexSdk() {
23
- if (!codexSdkPromise) {
24
- codexSdkPromise = import("@northflare/codex-sdk");
25
- }
26
- return codexSdkPromise;
27
- }
28
- export class CodexManager {
29
- runner;
30
- repositoryManager;
31
- threadStates = new Map();
32
- constructor(runner, repositoryManager) {
33
- this.runner = runner;
34
- this.repositoryManager = repositoryManager;
35
- }
36
- async startConversation(conversationObjectType, conversationObjectId, config, initialMessages, conversationData, provider) {
37
- if (!conversationData?.id) {
38
- throw new Error("startConversation requires conversationData with a valid conversation.id");
39
- }
40
- const conversationId = conversationData.id;
41
- const agentSessionId = this.resolveSessionId(config, conversationData);
42
- const pendingSummary = this.runner.consumePendingConversationSummary(conversationId);
43
- const rawSummary = typeof conversationData.summary === "string"
44
- ? conversationData.summary
45
- : pendingSummary;
46
- const normalizedSummary = typeof rawSummary === "string"
47
- ? rawSummary.replace(/\s+/g, " ").trim()
48
- : null;
49
- const rawModel = conversationData?.model ||
50
- config?.model ||
51
- config?.defaultModel ||
52
- "openai";
53
- const { baseModel, reasoningEffort } = parseModelValue(rawModel);
54
- const codexReasoningEffort = mapReasoningEffortForCodex(reasoningEffort);
55
- const normalizedModel = baseModel || rawModel;
56
- // Debug logging for model parsing
57
- console.log("[CodexManager] Model parsing debug:", {
58
- rawModel,
59
- baseModel,
60
- reasoningEffort,
61
- codexReasoningEffort,
62
- normalizedModel,
63
- });
64
- // Determine the actual provider - can be passed explicitly or detected from config
65
- const resolvedProvider = provider ||
66
- config?.providerType ||
67
- conversationData?.providerType ||
68
- "openai";
69
- // Optional system prompt handling (matches Northflare/Claude managers)
70
- const providedSystemPrompt = buildSystemPromptWithNorthflare(config.systemPrompt || conversationData?.systemPrompt, config?.northflareSystemPrompt ||
71
- conversationData?.northflareSystemPrompt);
72
- const systemPromptMode = config.systemPromptMode ||
73
- conversationData?.systemPromptMode ||
74
- "append";
75
- const metadata = {
76
- instructionsInjected: false,
77
- originalModelValue: rawModel,
78
- };
79
- metadata["hasSystemPrompt"] = Boolean(providedSystemPrompt);
80
- metadata["systemPromptMode"] = systemPromptMode;
81
- if (reasoningEffort) {
82
- metadata["modelReasoningEffort"] = reasoningEffort;
83
- }
84
- const context = {
85
- conversationId,
86
- agentSessionId,
87
- conversationObjectType,
88
- conversationObjectId,
89
- taskId: conversationObjectType === "Task" ? conversationObjectId : undefined,
90
- workspaceId: config.workspaceId,
91
- status: "starting",
92
- config,
93
- startedAt: new Date(),
94
- lastActivityAt: new Date(),
95
- model: normalizedModel,
96
- summary: normalizedSummary,
97
- globalInstructions: conversationData.globalInstructions || "",
98
- workspaceInstructions: conversationData.workspaceInstructions || "",
99
- permissionsMode: conversationData.permissionsMode ||
100
- config?.permissionsMode ||
101
- "all",
102
- provider: resolvedProvider,
103
- metadata,
104
- };
105
- this.runner.activeConversations_.set(conversationId, context);
106
- console.log(`[CodexManager] Stored conversation context`, {
107
- conversationId,
108
- agentSessionId,
109
- model: context.model,
110
- permissionsMode: context.permissionsMode,
111
- });
112
- const workspacePath = await this.resolveWorkspacePath(context, config);
113
- context.metadata["workspacePath"] = workspacePath;
114
- const githubToken = context.workspaceId
115
- ? await this.fetchGithubTokens(context.workspaceId)
116
- : undefined;
117
- const toolToken = this.generateToolToken(context);
118
- let mcpServers;
119
- if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
120
- const expandedServers = expandEnv(config.mcpServers, {
121
- TOOL_TOKEN: toolToken,
122
- });
123
- mcpServers = this.normalizeMcpServersForCodex(expandedServers);
124
- console.log("[CodexManager] MCP servers configuration:", JSON.stringify(mcpServers, null, 2));
125
- context.metadata["mcpServers"] = mcpServers;
126
- }
127
- let configOverrides = this.buildConfigOverridesFromMcp(mcpServers);
128
- // Add OpenRouter config overrides if using OpenRouter provider
129
- if (resolvedProvider === "openrouter") {
130
- const openRouterOverrides = this.buildOpenRouterConfigOverrides();
131
- configOverrides = {
132
- ...configOverrides,
133
- ...openRouterOverrides,
134
- };
135
- console.log("[CodexManager] OpenRouter config overrides:", JSON.stringify(openRouterOverrides, null, 2));
136
- }
137
- // Inject system prompt via developer_instructions (developer role message)
138
- if (providedSystemPrompt) {
139
- const trimmedPrompt = providedSystemPrompt.trim();
140
- if (trimmedPrompt.length) {
141
- const systemPromptOverrides = {
142
- developer_instructions: trimmedPrompt,
143
- };
144
- configOverrides = {
145
- ...configOverrides,
146
- ...systemPromptOverrides,
147
- };
148
- console.log("[CodexManager] Applied system prompt overrides", {
149
- mode: systemPromptMode,
150
- hasSystemPrompt: true,
151
- replacesBase: systemPromptMode === "replace",
152
- });
153
- }
154
- }
155
- const envVars = await this.buildEnvVars(config, githubToken, toolToken, resolvedProvider);
156
- const { Codex } = await loadCodexSdk();
157
- const codex = new Codex({
158
- baseUrl: config?.openaiBaseUrl ||
159
- config?.codexBaseUrl ||
160
- process.env["OPENAI_BASE_URL"],
161
- apiKey: config?.openaiApiKey ||
162
- config?.codexApiKey ||
163
- process.env["CODEX_API_KEY"] ||
164
- process.env["OPENAI_API_KEY"],
165
- env: envVars,
166
- });
167
- // Strip provider prefix (e.g., "openrouter:") from model name if present
168
- const modelName = context.model.includes(":")
169
- ? context.model.split(":").slice(1).join(":")
170
- : context.model;
171
- // Debug: log what model name we're using
172
- console.log("[CodexManager] Model name processing:", {
173
- contextModel: context.model,
174
- hasColon: context.model.includes(":"),
175
- modelNameAfterStrip: modelName,
176
- });
177
- const threadOptions = {
178
- model: modelName,
179
- workingDirectory: workspacePath,
180
- sandboxMode: this.mapSandboxMode(context.permissionsMode),
181
- networkAccessEnabled: this.shouldEnableNetwork(context.permissionsMode),
182
- webSearchEnabled: true,
183
- // additionalDirectories: this.getAdditionalDirectories(config),
184
- configOverrides,
185
- skipGitRepoCheck: true,
186
- modelReasoningEffort: codexReasoningEffort,
187
- };
188
- console.log("[CodexManager] Thread options:", JSON.stringify(threadOptions, null, 2));
189
- const thread = agentSessionId
190
- ? codex.resumeThread(agentSessionId, threadOptions)
191
- : codex.startThread(threadOptions);
192
- this.threadStates.set(conversationId, {
193
- thread,
194
- abortController: null,
195
- runPromise: null,
196
- });
197
- context.conversation = thread;
198
- // Launch the first turn using the provided initial messages.
199
- // Supports multimodal (images/documents) by converting to Codex Input.
200
- if (initialMessages?.length) {
201
- const initialInput = await this.buildInitialInput(initialMessages, context);
202
- this.launchTurn(context, initialInput);
203
- }
204
- return context;
205
- }
206
- async stopConversation(_agentSessionId, context, isRunnerShutdown = false, reason) {
207
- context._stopRequested = true;
208
- context._stopReason =
209
- reason || (isRunnerShutdown ? "runner_shutdown" : undefined);
210
- context.status = "stopping";
211
- await this.abortActiveRun(context.conversationId);
212
- try {
213
- await this._finalizeConversation(context, false, undefined, context._stopReason);
214
- }
215
- catch (error) {
216
- console.error(`[CodexManager] Error finalizing conversation ${context.conversationId}:`, error);
217
- }
218
- finally {
219
- this.threadStates.delete(context.conversationId);
220
- }
221
- context.status = "stopped";
222
- }
223
- async resumeConversation(conversationObjectType, conversationObjectId, agentSessionId, config, conversationData, resumeMessage, provider) {
224
- console.log(`[CodexManager] Resuming conversation ${agentSessionId}`);
225
- const context = await this.startConversation(conversationObjectType, conversationObjectId, { ...config, sessionId: agentSessionId }, [], conversationData, provider);
226
- if (resumeMessage) {
227
- const prompt = this.buildPromptForMessage(context, resumeMessage, false);
228
- this.launchTurn(context, prompt);
229
- }
230
- return context.agentSessionId;
231
- }
232
- async sendUserMessage(conversationId, content, config, conversationObjectType, conversationObjectId, conversation, agentSessionIdOverride, provider) {
233
- console.log(`[CodexManager] sendUserMessage called`, {
234
- conversationId,
235
- hasConfig: !!config,
236
- });
237
- let context = this.runner.getConversationContext(conversationId);
238
- if (!context && conversation) {
239
- const conversationDetails = conversation;
240
- const resumeSessionId = this.resolveSessionId(config, conversationDetails, agentSessionIdOverride);
241
- const startConfig = {
242
- ...(config || {}),
243
- workspaceId: conversationDetails.workspaceId || config?.workspaceId || undefined,
244
- ...(resumeSessionId ? { sessionId: resumeSessionId } : {}),
245
- };
246
- context = await this.startConversation(conversationObjectType ||
247
- conversationDetails.objectType, conversationObjectId || conversationDetails.objectId, startConfig, [], conversationDetails, provider);
248
- }
249
- if (!context) {
250
- throw new Error(`No active or fetchable conversation found for ${conversationId}`);
251
- }
252
- await this.abortActiveRun(conversationId);
253
- const input = await this.normalizeToInput(content, context, false);
254
- this.launchTurn(context, input);
255
- context.lastActivityAt = new Date();
256
- }
257
- async _handleRunCompletion(context, hadError, error) {
258
- if (context._finalized)
259
- return;
260
- const inFlight = context._runCompletionInFlight;
261
- if (inFlight)
262
- return inFlight;
263
- const promise = (async () => {
264
- const isTaskConversation = context.conversationObjectType === "Task";
265
- const stopRequested = context._stopRequested === true;
266
- const taskHandle = context.taskHandle;
267
- const shouldRunGitFlow = context.config.useWorktrees !== false &&
268
- isTaskConversation &&
269
- !!taskHandle &&
270
- taskHandle.branch !== "local";
271
- if (!shouldRunGitFlow || hadError || stopRequested) {
272
- await this._finalizeConversation(context, hadError, error);
273
- return;
274
- }
275
- const taskId = context.conversationObjectId;
276
- const baseBranch = context.config.repository?.branch || "main";
277
- try {
278
- const taskState = await this.repositoryManager.getTaskState(taskId);
279
- const taskRepoInfo = await this.repositoryManager.getTaskRepoInfo(taskId);
280
- if (!taskState ||
281
- !taskRepoInfo ||
282
- !taskRepoInfo.worktreePath ||
283
- taskState.branch === "local") {
284
- await this._finalizeConversation(context, false);
285
- return;
286
- }
287
- const isLocalRepo = taskRepoInfo.repoKey.startsWith("local__");
288
- const mergeWorkdir = isLocalRepo
289
- ? taskRepoInfo.controlPath
290
- : taskRepoInfo.worktreePath;
291
- // If we already have unresolved conflicts, immediately ask the agent to resolve them.
292
- const taskGit = simpleGit(taskRepoInfo.worktreePath);
293
- const initialTaskStatus = await taskGit.status();
294
- if (initialTaskStatus.conflicted.length > 0) {
295
- await this._resumeForGitConflictResolution(context, {
296
- kind: "conflicts",
297
- baseBranch,
298
- conflicts: initialTaskStatus.conflicted,
299
- workdir: taskRepoInfo.worktreePath,
300
- });
301
- return;
302
- }
303
- if (isLocalRepo && mergeWorkdir !== taskRepoInfo.worktreePath) {
304
- const mergeGit = simpleGit(mergeWorkdir);
305
- const mergeStatus = await mergeGit.status();
306
- if (mergeStatus.conflicted.length > 0) {
307
- await this._resumeForGitConflictResolution(context, {
308
- kind: "conflicts",
309
- baseBranch,
310
- conflicts: mergeStatus.conflicted,
311
- workdir: mergeWorkdir,
312
- });
313
- return;
314
- }
315
- }
316
- // Ensure we're on the task branch for commit/rebase operations.
317
- const currentBranch = (await taskGit.revparse(["--abbrev-ref", "HEAD"])).trim();
318
- if (currentBranch !== taskState.branch) {
319
- await taskGit.checkout(taskState.branch);
320
- }
321
- const status = await taskGit.status();
322
- if (!status.isClean()) {
323
- await this.repositoryManager.stageAll(taskId);
324
- try {
325
- const previousCommitCount = taskState?.commitCount ?? 0;
326
- const baseSubject = (context.summary ?? "").replace(/\s+/g, " ").trim() ||
327
- `Task ${taskId}`;
328
- const message = previousCommitCount > 0
329
- ? `${baseSubject} (followup #${previousCommitCount})`
330
- : baseSubject;
331
- await this.repositoryManager.commit(taskId, message);
332
- }
333
- catch (commitError) {
334
- const msg = commitError instanceof Error
335
- ? commitError.message
336
- : String(commitError);
337
- if (!msg.toLowerCase().includes("nothing to commit")) {
338
- throw commitError;
339
- }
340
- }
341
- }
342
- const integrateResult = await this.repositoryManager.integrateTask(taskId, baseBranch, "no-ff");
343
- if (!integrateResult.success) {
344
- await this._resumeForGitConflictResolution(context, {
345
- kind: integrateResult.phase,
346
- baseBranch,
347
- conflicts: integrateResult.conflicts ?? [],
348
- workdir: integrateResult.conflictWorkdir,
349
- error: integrateResult.error,
350
- });
351
- return;
352
- }
353
- // Worktree is no longer needed after a successful merge; preserve the branch
354
- await this.repositoryManager.removeTaskWorktree(taskId, {
355
- preserveBranch: true,
356
- });
357
- const wsId = context.config.workspaceId || context.workspaceId;
358
- const repoUrl = context.config.repository?.url;
359
- if (wsId && repoUrl) {
360
- await this.repositoryManager.syncWorkspaceWorktree(wsId, repoUrl, baseBranch);
361
- }
362
- await this._finalizeConversation(context, false);
363
- }
364
- catch (mergeError) {
365
- console.error("[CodexManager] Post-task git flow failed", mergeError);
366
- await this._finalizeConversation(context, true, mergeError);
367
- }
368
- })();
369
- context._runCompletionInFlight = promise.finally(() => {
370
- delete context._runCompletionInFlight;
371
- });
372
- return context._runCompletionInFlight;
373
- }
374
- async _resumeForGitConflictResolution(context, payload) {
375
- const { kind, baseBranch, conflicts, workdir, error } = payload;
376
- const conflictsList = conflicts?.length
377
- ? `\n\nConflicted files:\n${conflicts.map((f) => `- ${f}`).join("\n")}`
378
- : "";
379
- const workdirHint = workdir ? `\n\nWork directory:\n- ${workdir}` : "";
380
- const errorHint = error ? `\n\nError:\n${error}` : "";
381
- const hasConflicts = !!conflicts?.length;
382
- let instruction;
383
- if (kind === "rebase") {
384
- instruction = hasConflicts
385
- ? `<system-instructions>\nA git rebase onto \`${baseBranch}\` produced merge conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory, then run:\n- git add -A\n- git rebase --continue\n\nWhen the rebase finishes cleanly, reply briefly so I can retry the integration.\n</system-instructions>`
386
- : `<system-instructions>\nA git rebase onto \`${baseBranch}\` failed.${workdirHint}${errorHint}\n\nNo conflicted files were reported. Please run:\n- git status\n\nFix the error (or complete/abort any in-progress git operation) and reply briefly so I can retry the integration.\n</system-instructions>`;
387
- }
388
- else if (kind === "merge") {
389
- instruction = hasConflicts
390
- ? `<system-instructions>\nA git merge into \`${baseBranch}\` produced merge conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory, then run:\n- git add -A\n- git commit -m \"Resolve merge conflicts for ${context.conversationObjectId}\"\n\nWhen the working tree is clean, reply briefly so I can retry the integration.\n</system-instructions>`
391
- : `<system-instructions>\nA git merge into \`${baseBranch}\` failed.${workdirHint}${errorHint}\n\nNo conflicted files were reported. Please run:\n- git status\n\nFix the error (or complete/abort any in-progress git operation) and reply briefly so I can retry the integration.\n</system-instructions>`;
392
- }
393
- else {
394
- instruction = `<system-instructions>\nGit has unresolved conflicts.${conflictsList}${workdirHint}${errorHint}\n\nPlease resolve the conflicts in that directory and complete the in-progress git operation (rebase or merge). When the working tree is clean, reply briefly so I can retry the integration.\n</system-instructions>`;
395
- }
396
- const conversationData = {
397
- id: context.conversationId,
398
- objectType: context.conversationObjectType,
399
- objectId: context.conversationObjectId,
400
- model: context.model,
401
- summary: context.summary ?? null,
402
- globalInstructions: context.globalInstructions,
403
- workspaceInstructions: context.workspaceInstructions,
404
- permissionsMode: context.permissionsMode,
405
- agentSessionId: context.agentSessionId,
406
- workspaceId: context.workspaceId,
407
- };
408
- await this.resumeConversation(context.conversationObjectType, context.conversationObjectId, context.agentSessionId, context.config, conversationData, instruction, context.provider);
409
- }
410
- async buildEnvVars(config, githubToken, toolToken, provider) {
411
- const envVars = {
412
- ...Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined)),
413
- };
414
- if (config.codexAuth?.accessToken) {
415
- envVars["OPENAI_ACCESS_TOKEN"] = config.codexAuth.accessToken;
416
- }
417
- else if (config.accessToken) {
418
- envVars["OPENAI_ACCESS_TOKEN"] = config.accessToken;
419
- }
420
- if (githubToken) {
421
- envVars["GITHUB_TOKEN"] = githubToken;
422
- }
423
- if (toolToken) {
424
- envVars["TOOL_TOKEN"] = toolToken;
425
- }
426
- if (isDebugEnabledFor("sdk")) {
427
- envVars["DEBUG"] = "1";
428
- }
429
- // Add OpenRouter/Groq API keys when using Northflare-routed providers
430
- if (provider === "openrouter" || provider === "groq") {
431
- const openRouterApiKey = config.openRouterApiKey ||
432
- process.env["OPENROUTER_API_KEY"];
433
- if (openRouterApiKey) {
434
- envVars["OPENROUTER_API_KEY"] = openRouterApiKey;
435
- }
436
- const groqApiKey = config.groqApiKey ||
437
- process.env["GROQ_API_KEY"];
438
- if (groqApiKey) {
439
- envVars["GROQ_API_KEY"] = groqApiKey;
440
- }
441
- if (!openRouterApiKey && !groqApiKey) {
442
- console.warn("[CodexManager] OpenRouter/Groq provider selected but no API key found");
443
- }
444
- }
445
- const codexHome = await this.ensureCodexAuthHome(config);
446
- if (codexHome) {
447
- envVars["CODEX_HOME"] = codexHome;
448
- }
449
- return envVars;
450
- }
451
- async ensureCodexAuthHome(config) {
452
- if (!config.codexAuth) {
453
- return null;
454
- }
455
- const runnerId = this.runner.getRunnerId();
456
- const dataDir = this.runner.config_.dataDir;
457
- if (!runnerId || !dataDir) {
458
- console.warn("[CodexManager] Missing runnerId or dataDir; cannot prepare Codex auth directory");
459
- return null;
460
- }
461
- const codexDir = path.join(dataDir, "codex", runnerId);
462
- const authPayload = {
463
- OPENAI_API_KEY: null,
464
- tokens: {
465
- id_token: config.codexAuth.idToken,
466
- access_token: config.codexAuth.accessToken,
467
- refresh_token: "",
468
- account_id: config.codexAuth.accountId,
469
- },
470
- last_refresh: config.codexAuth.lastRefresh || new Date().toISOString(),
471
- };
472
- try {
473
- await fs.mkdir(codexDir, { recursive: true });
474
- await fs.writeFile(path.join(codexDir, "auth.json"), JSON.stringify(authPayload, null, 2), "utf-8");
475
- return codexDir;
476
- }
477
- catch (error) {
478
- console.error("[CodexManager] Failed to persist Codex auth configuration:", error);
479
- throw new Error("Runner failed to persist Codex credentials. Check runner logs for details.");
480
- }
481
- }
482
- buildPromptForMessage(context, text, forceInstructions) {
483
- const trimmed = text ?? "";
484
- const instructions = this.getInstructionPrefix(context);
485
- const metadata = context.metadata || {};
486
- const shouldInjectInstructions = forceInstructions || !metadata["instructionsInjected"];
487
- if (instructions && shouldInjectInstructions) {
488
- metadata["instructionsInjected"] = true;
489
- context.metadata = metadata;
490
- return `${instructions}\n\n${trimmed}`.trim();
491
- }
492
- return trimmed;
493
- }
494
- getInstructionPrefix(context) {
495
- const parts = [];
496
- if (context.globalInstructions) {
497
- parts.push(`<global-instructions>\n${context.globalInstructions}\n</global-instructions>`);
498
- }
499
- if (context.workspaceInstructions) {
500
- parts.push(`<workspace-instructions>\n${context.workspaceInstructions}\n</workspace-instructions>`);
501
- }
502
- return parts.join("\n\n");
503
- }
504
- formatInitialMessages(initialMessages) {
505
- return initialMessages
506
- .map((msg) => {
507
- const role = msg.role || "user";
508
- const content = this.normalizeToText(msg.content);
509
- return `<${role}-message>\n${content}\n</${role}-message>`;
510
- })
511
- .join("\n\n");
512
- }
513
- async buildInitialInput(initialMessages, context) {
514
- const hasMultimodal = initialMessages.some((msg) => this.hasMultimodalContent(msg.content));
515
- if (!hasMultimodal) {
516
- const promptText = this.formatInitialMessages(initialMessages);
517
- return this.buildPromptForMessage(context, promptText, true);
518
- }
519
- // Preserve role markers while keeping image blocks attached
520
- const combinedBlocks = [];
521
- for (const msg of initialMessages) {
522
- const role = msg.role || "user";
523
- const content = msg.content;
524
- // Start role section
525
- combinedBlocks.push({
526
- type: "text",
527
- text: `<${role}-message>`,
528
- });
529
- if (Array.isArray(content)) {
530
- for (const item of content) {
531
- if (!item || typeof item !== "object")
532
- continue;
533
- if (item.type === "text" && typeof item.text === "string") {
534
- combinedBlocks.push({ type: "text", text: item.text });
535
- }
536
- else if (item.type === "image" || item.type === "document") {
537
- combinedBlocks.push(item);
538
- }
539
- }
540
- }
541
- else {
542
- combinedBlocks.push({
543
- type: "text",
544
- text: this.normalizeToText(content),
545
- });
546
- }
547
- // Close role section
548
- combinedBlocks.push({
549
- type: "text",
550
- text: `</${role}-message>`,
551
- });
552
- }
553
- const input = await this.normalizeToInput(combinedBlocks, context, true);
554
- if (isDebugEnabledFor("manager") || isDebugEnabledFor("sdk")) {
555
- console.log("[CodexManager] buildInitialInput multimodal payload", {
556
- conversationId: context.conversationId,
557
- blocks: combinedBlocks.map((b) => b.type),
558
- inputType: Array.isArray(input) ? "array" : "string",
559
- textItems: Array.isArray(input)
560
- ? input.filter((i) => i.type === "text").length
561
- : 1,
562
- imageItems: Array.isArray(input)
563
- ? input.filter((i) => i.type === "local_image").length
564
- : 0,
565
- });
566
- }
567
- return input;
568
- }
569
- launchTurn(context, prompt) {
570
- const state = this.threadStates.get(context.conversationId);
571
- if (!state || !state.thread) {
572
- throw new Error(`Thread state missing for conversation ${context.conversationId}`);
573
- }
574
- const abortController = new AbortController();
575
- state.abortController = abortController;
576
- context.lastActivityAt = new Date();
577
- const runPromise = this.streamThreadEvents(context, state.thread, prompt, abortController);
578
- state.runPromise = runPromise;
579
- runPromise
580
- .catch((error) => {
581
- if (!this.isAbortError(error)) {
582
- console.error(`[CodexManager] Run failed for ${context.conversationId}:`, error);
583
- this._handleConversationError(context, error);
584
- }
585
- })
586
- .finally(() => {
587
- if (state.runPromise === runPromise) {
588
- state.runPromise = null;
589
- }
590
- if (state.abortController === abortController) {
591
- state.abortController = null;
592
- }
593
- });
594
- }
595
- async streamThreadEvents(context, thread, prompt, abortController) {
596
- try {
597
- const { events } = await thread.runStreamed(prompt, {
598
- signal: abortController.signal,
599
- });
600
- for await (const event of events) {
601
- await this.handleThreadEvent(context, event);
602
- }
603
- }
604
- catch (error) {
605
- if (this.isAbortError(error)) {
606
- console.log(`[CodexManager] Turn aborted for ${context.conversationId}`);
607
- return;
608
- }
609
- throw error;
610
- }
611
- }
612
- async abortActiveRun(conversationId) {
613
- const state = this.threadStates.get(conversationId);
614
- if (!state || !state.runPromise)
615
- return;
616
- if (state.abortController) {
617
- state.abortController.abort();
618
- }
619
- try {
620
- await state.runPromise;
621
- }
622
- catch (error) {
623
- if (!this.isAbortError(error)) {
624
- console.warn(`[CodexManager] Run aborted with error for ${conversationId}:`, error);
625
- }
626
- }
627
- }
628
- async handleThreadEvent(context, event) {
629
- try {
630
- this.logRawThreadEvent(event);
631
- switch (event.type) {
632
- case "thread.started": {
633
- await this.handleThreadStarted(context, event.thread_id);
634
- break;
635
- }
636
- case "turn.started": {
637
- context.status = "active";
638
- await this.sendAgentMessage(context, "system", {
639
- subtype: "turn.started",
640
- content: [
641
- {
642
- type: "text",
643
- text: `Turn started at ${new Date().toISOString()}`,
644
- },
645
- ],
646
- });
647
- break;
648
- }
649
- case "turn.completed": {
650
- await this.sendAgentMessage(context, "result", {
651
- subtype: "turn.completed",
652
- content: [
653
- {
654
- type: "usage",
655
- usage: event.usage,
656
- },
657
- ],
658
- });
659
- context.status = "stopped";
660
- await this._handleRunCompletion(context, false);
661
- break;
662
- }
663
- case "turn.failed": {
664
- const error = new Error(event.error?.message || "Turn failed");
665
- await this.sendAgentMessage(context, "error", {
666
- subtype: "turn.failed",
667
- content: [
668
- {
669
- type: "text",
670
- text: error.message,
671
- },
672
- ],
673
- isError: true,
674
- });
675
- await this._handleConversationError(context, error);
676
- context.status = "stopped";
677
- await this._finalizeConversation(context, true, error, "turn_failed");
678
- break;
679
- }
680
- case "item.started":
681
- case "item.updated":
682
- case "item.completed": {
683
- await this.forwardItemEvent(context, event.item, event.type.split(".")[1]);
684
- break;
685
- }
686
- case "error": {
687
- const fatalError = new Error(event.message || "Unknown error");
688
- await this.sendAgentMessage(context, "error", {
689
- subtype: "thread.error",
690
- content: [{ type: "text", text: fatalError.message }],
691
- isError: true,
692
- });
693
- await this._handleConversationError(context, fatalError);
694
- context.status = "stopped";
695
- await this._finalizeConversation(context, true, fatalError, "thread_error");
696
- break;
697
- }
698
- }
699
- }
700
- catch (error) {
701
- console.error("[CodexManager] Failed to handle thread event", {
702
- event,
703
- error,
704
- });
705
- }
706
- }
707
- async forwardItemEvent(context, item, phase) {
708
- const normalized = this.normalizeItemEvent(context, item, phase);
709
- if (!normalized)
710
- return;
711
- const { subtype, content, isError, toolCalls, type } = normalized;
712
- const payload = {
713
- subtype,
714
- content,
715
- isError,
716
- };
717
- if (toolCalls) {
718
- payload.toolCalls = toolCalls;
719
- }
720
- await this.sendAgentMessage(context, type, payload);
721
- }
722
- normalizeItemEvent(context, item, phase) {
723
- switch (item.type) {
724
- case "agent_message": {
725
- if (phase !== "completed")
726
- return null;
727
- return {
728
- type: "assistant",
729
- content: [
730
- {
731
- type: "text",
732
- text: item.text || "",
733
- },
734
- ],
735
- };
736
- }
737
- case "reasoning": {
738
- return {
739
- type: "thinking",
740
- content: [
741
- {
742
- type: "thinking",
743
- thinking: item.text,
744
- text: item.text,
745
- phase,
746
- },
747
- ],
748
- };
749
- }
750
- case "command_execution": {
751
- // Namespace command_execution tool use IDs so they don't collide
752
- // with MCP tool call IDs that may reuse the same raw item.id.
753
- const internalNamespace = "codex_command";
754
- const toolUseId = this.buildToolUseId(context, `${internalNamespace}:${item.id}`);
755
- const timestamp = new Date().toISOString();
756
- const isTerminal = phase === "completed" ||
757
- item.status === "completed" ||
758
- item.status === "failed";
759
- if (!isTerminal) {
760
- if (phase !== "started") {
761
- return null;
762
- }
763
- return {
764
- type: "assistant",
765
- subtype: "tool_use",
766
- content: [
767
- {
768
- toolCalls: [
769
- {
770
- id: toolUseId,
771
- name: "codex_command",
772
- arguments: {
773
- command: item.command,
774
- status: item.status,
775
- },
776
- status: item.status,
777
- },
778
- ],
779
- timestamp,
780
- },
781
- ],
782
- };
783
- }
784
- const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
785
- const isError = item.status === "failed" || (exitCode ?? 0) !== 0;
786
- return {
787
- type: "tool_result",
788
- subtype: "tool_result",
789
- content: [
790
- {
791
- type: "tool_result",
792
- tool_use_id: toolUseId,
793
- content: {
794
- kind: "codex_command_result",
795
- command: item.command,
796
- output: item.aggregated_output || "",
797
- exitCode,
798
- status: item.status,
799
- },
800
- timestamp,
801
- },
802
- ],
803
- isError,
804
- };
805
- }
806
- case "file_change": {
807
- if (phase === "updated")
808
- return null;
809
- return {
810
- type: "file_change",
811
- subtype: "file_change",
812
- content: [
813
- {
814
- type: "file_change",
815
- changes: item.changes,
816
- status: item.status,
817
- },
818
- ],
819
- };
820
- }
821
- case "mcp_tool_call": {
822
- const toolName = this.buildMcpToolName(item.server, item.tool);
823
- // Namespace MCP tool call IDs with the MCP tool name so they don't
824
- // collide with internal tool IDs or MCP calls from other servers.
825
- const namespacedRawId = `mcp:${toolName}:${item.id}`;
826
- const toolUseId = this.buildToolUseId(context, namespacedRawId);
827
- if (item.status === "in_progress" || phase !== "completed") {
828
- return {
829
- type: "assistant",
830
- subtype: "tool_use",
831
- content: [
832
- {
833
- toolCalls: [
834
- {
835
- id: toolUseId,
836
- name: toolName,
837
- arguments: item.arguments,
838
- server: item.server,
839
- tool: item.tool,
840
- status: item.status,
841
- },
842
- ],
843
- timestamp: new Date().toISOString(),
844
- },
845
- ],
846
- };
847
- }
848
- if (item.status === "failed") {
849
- return {
850
- type: "error",
851
- subtype: "tool_result",
852
- content: [
853
- {
854
- type: "text",
855
- text: item.error?.message || "Tool call failed",
856
- tool_use_id: toolUseId,
857
- tool_name: toolName,
858
- timestamp: new Date().toISOString(),
859
- },
860
- ],
861
- isError: true,
862
- };
863
- }
864
- return {
865
- type: "tool_result",
866
- subtype: "tool_result",
867
- content: [
868
- {
869
- type: "tool_result",
870
- subtype: "tool_result",
871
- tool_use_id: toolUseId,
872
- content: item.result?.content || [],
873
- structured_content: item.result?.structured_content,
874
- metadata: {
875
- server: item.server,
876
- tool: item.tool,
877
- name: toolName,
878
- original_tool_use_id: item.id,
879
- },
880
- timestamp: new Date().toISOString(),
881
- },
882
- ],
883
- };
884
- }
885
- case "web_search": {
886
- return {
887
- type: "system",
888
- subtype: `web_search.${phase}`,
889
- content: [
890
- {
891
- type: "web_search",
892
- query: item.query,
893
- status: phase,
894
- },
895
- ],
896
- };
897
- }
898
- case "todo_list": {
899
- return {
900
- type: "system",
901
- subtype: "todo_list",
902
- content: [
903
- {
904
- type: "todo_list",
905
- items: item.items,
906
- phase,
907
- },
908
- ],
909
- };
910
- }
911
- case "error": {
912
- return {
913
- type: "error",
914
- subtype: `item.${phase}`,
915
- content: [
916
- {
917
- type: "text",
918
- text: item.message,
919
- },
920
- ],
921
- isError: true,
922
- };
923
- }
924
- default:
925
- return null;
926
- }
927
- }
928
- async sendAgentMessage(context, type, { subtype, content, toolCalls, isError, }) {
929
- const normalizedContent = Array.isArray(content)
930
- ? content
931
- : content
932
- ? [content]
933
- : [];
934
- const payload = {
935
- taskId: context.taskId,
936
- conversationId: context.conversationId,
937
- conversationObjectType: context.conversationObjectType,
938
- conversationObjectId: context.conversationObjectId,
939
- agentSessionId: context.agentSessionId,
940
- type,
941
- subtype,
942
- content: normalizedContent,
943
- toolCalls,
944
- isError: Boolean(isError),
945
- messageId: this.generateMessageId(context),
946
- timestamp: new Date().toISOString(),
947
- };
948
- if (isDebugEnabledFor("manager")) {
949
- console.log("[CodexManager] Sending message.agent", payload);
950
- }
951
- await this.runner.notify("message.agent", payload);
952
- }
953
- async handleThreadStarted(context, threadId) {
954
- if (!threadId || threadId === context.agentSessionId) {
955
- return;
956
- }
957
- const oldSessionId = context.agentSessionId;
958
- context.agentSessionId = threadId;
959
- await this.runner.notify("agentSessionId.changed", {
960
- conversationId: context.conversationId,
961
- conversationObjectType: context.conversationObjectType,
962
- conversationObjectId: context.conversationObjectId,
963
- oldAgentSessionId: oldSessionId,
964
- newAgentSessionId: threadId,
965
- });
966
- }
967
- mapSandboxMode(permissionsMode) {
968
- const mode = (permissionsMode || "").toLowerCase();
969
- if (mode === "read" || mode === "read_only") {
970
- return "read-only";
971
- }
972
- // "project" mode allows bash but no file writes - Codex doesn't have fine-grained control,
973
- // so we use workspace-write which allows both bash and file writes
974
- if (mode === "project" || mode === "workspace" || mode === "workspace-write") {
975
- return "workspace-write";
976
- }
977
- return "danger-full-access";
978
- }
979
- shouldEnableNetwork(permissionsMode) {
980
- const mode = (permissionsMode || "").toLowerCase();
981
- // project mode allows network (for fetching dependencies, running tests, etc.)
982
- if (mode === "project")
983
- return true;
984
- return mode !== "read" && mode !== "read_only";
985
- }
986
- getAdditionalDirectories(config) {
987
- const additionalDirs = [];
988
- if (config.runnerRepoPath) {
989
- additionalDirs.push(config.runnerRepoPath);
990
- }
991
- return additionalDirs.length ? additionalDirs : undefined;
992
- }
993
- buildConfigOverridesFromMcp(mcpServers) {
994
- if (!mcpServers)
995
- return undefined;
996
- const overrides = {};
997
- for (const [serverName, config] of Object.entries(mcpServers)) {
998
- this.flattenOverrideObject(`mcp_servers.${serverName}`, config, overrides);
999
- }
1000
- return Object.keys(overrides).length ? overrides : undefined;
1001
- }
1002
- /**
1003
- * Build Codex config overrides for OpenRouter provider.
1004
- * These correspond to the Codex CLI flags:
1005
- * -c model_provider=openrouter
1006
- * -c model_providers.openrouter.name=openrouter
1007
- * -c model_providers.openrouter.base_url=https://openrouter.ai/api/v1
1008
- * -c model_providers.openrouter.env_key=OPENROUTER_API_KEY
1009
- */
1010
- buildOpenRouterConfigOverrides() {
1011
- return {
1012
- model_provider: "openrouter",
1013
- "model_providers.openrouter.name": "openrouter",
1014
- "model_providers.openrouter.base_url": "https://openrouter.ai/api/v1",
1015
- "model_providers.openrouter.env_key": "OPENROUTER_API_KEY",
1016
- };
1017
- }
1018
- normalizeMcpServersForCodex(mcpServers) {
1019
- const normalized = {};
1020
- for (const [serverName, config] of Object.entries(mcpServers)) {
1021
- normalized[serverName] = this.stripAuthorizationHeader(config);
1022
- }
1023
- return normalized;
1024
- }
1025
- buildMcpToolName(server, tool) {
1026
- const safeServer = (server || "unknown").trim() || "unknown";
1027
- const safeTool = (tool || "unknown").trim() || "unknown";
1028
- return `mcp__${safeServer}__${safeTool}`;
1029
- }
1030
- logRawThreadEvent(event) {
1031
- if (!isDebugEnabledFor("sdk")) {
1032
- return;
1033
- }
1034
- try {
1035
- console.log("[CodexManager] RAW Codex event FULL:", this.safeStringify(event));
1036
- const summary = {
1037
- type: event?.type,
1038
- keys: Object.keys(event || {}),
1039
- hasItem: Boolean(event?.item),
1040
- itemType: event?.item?.type,
1041
- hasUsage: Boolean(event?.usage),
1042
- threadId: event?.thread_id,
1043
- turnId: event?.turn_id,
1044
- };
1045
- console.log("[CodexManager] RAW Codex event summary:", summary);
1046
- if (event?.item) {
1047
- console.log("[CodexManager] RAW Codex event item:", this.safeStringify(event.item));
1048
- }
1049
- if (event?.usage) {
1050
- console.log("[CodexManager] RAW Codex usage:", event.usage);
1051
- }
1052
- }
1053
- catch (error) {
1054
- console.warn("[CodexManager] Failed to log raw Codex event:", error);
1055
- }
1056
- }
1057
- safeStringify(value) {
1058
- const seen = new WeakSet();
1059
- return JSON.stringify(value, (key, nested) => {
1060
- if (typeof nested === "function")
1061
- return undefined;
1062
- if (typeof nested === "bigint")
1063
- return nested.toString();
1064
- if (typeof nested === "object" && nested !== null) {
1065
- if (seen.has(nested))
1066
- return "[Circular]";
1067
- seen.add(nested);
1068
- }
1069
- return nested;
1070
- }, 2);
1071
- }
1072
- buildToolUseId(context, rawId) {
1073
- const scope = context.agentSessionId ||
1074
- context.conversationId ||
1075
- context.conversationObjectId ||
1076
- "codex";
1077
- return `${scope}:${rawId}`;
1078
- }
1079
- stripAuthorizationHeader(config) {
1080
- if (!config || typeof config !== "object" || Array.isArray(config)) {
1081
- return config;
1082
- }
1083
- const normalized = { ...config };
1084
- if (normalized.bearer_token_env_var &&
1085
- normalized.headers &&
1086
- typeof normalized.headers === "object" &&
1087
- !Array.isArray(normalized.headers)) {
1088
- const headers = { ...normalized.headers };
1089
- delete headers["Authorization"];
1090
- if (Object.keys(headers).length === 0) {
1091
- delete normalized.headers;
1092
- }
1093
- else {
1094
- normalized.headers = headers;
1095
- }
1096
- }
1097
- return normalized;
1098
- }
1099
- flattenOverrideObject(prefix, value, target) {
1100
- if (value === undefined) {
1101
- return;
1102
- }
1103
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1104
- target[prefix] = value;
1105
- return;
1106
- }
1107
- const entries = Object.entries(value);
1108
- if (!entries.length) {
1109
- target[prefix] = {};
1110
- return;
1111
- }
1112
- for (const [key, nested] of entries) {
1113
- this.flattenOverrideObject(`${prefix}.${key}`, nested, target);
1114
- }
1115
- }
1116
- async resolveWorkspacePath(context, config) {
1117
- let workspacePath;
1118
- const workspaceId = config.workspaceId;
1119
- const useWorktrees = config.useWorktrees !== false;
1120
- if (config.runnerRepoPath) {
1121
- workspacePath = config.runnerRepoPath;
1122
- console.log(`[CodexManager] Using local workspace path ${workspacePath}`);
1123
- if (context.conversationObjectType === "Task" && workspaceId) {
1124
- const hasGitDir = await fs
1125
- .access(path.join(workspacePath, ".git"))
1126
- .then(() => true)
1127
- .catch(() => false);
1128
- if (hasGitDir && useWorktrees) {
1129
- const repoUrl = config.repository?.url || `file://${workspacePath}`;
1130
- const baseBranch = config.repository?.branch || "main";
1131
- const taskHandle = await this.repositoryManager.createTaskWorktree(context.conversationObjectId, workspaceId, repoUrl, baseBranch, config.githubToken);
1132
- context.taskHandle = taskHandle;
1133
- workspacePath = taskHandle.worktreePath;
1134
- }
1135
- }
1136
- return workspacePath;
1137
- }
1138
- if (context.conversationObjectType === "Task" &&
1139
- config.repository &&
1140
- workspaceId &&
1141
- useWorktrees) {
1142
- if (!config.repository.url) {
1143
- throw new Error("Repository URL is required for task conversations");
1144
- }
1145
- const taskHandle = await this.repositoryManager.createTaskWorktree(context.conversationObjectId, workspaceId, config.repository.url, config.repository.branch, config.githubToken);
1146
- context.taskHandle = taskHandle;
1147
- workspacePath = taskHandle.worktreePath;
1148
- return workspacePath;
1149
- }
1150
- if (config.repository && workspaceId) {
1151
- if (config.repository.type === "local" && config.repository.localPath) {
1152
- workspacePath = config.repository.localPath;
1153
- return workspacePath;
1154
- }
1155
- workspacePath = await this.repositoryManager.checkoutRepository(workspaceId, config.repository.url, config.repository.branch, config.githubToken);
1156
- return workspacePath;
1157
- }
1158
- if (workspaceId) {
1159
- workspacePath =
1160
- await this.repositoryManager.getWorkspacePath(workspaceId);
1161
- return workspacePath;
1162
- }
1163
- return process.cwd();
1164
- }
1165
- async fetchGithubTokens(workspaceId) {
1166
- try {
1167
- const response = await fetch(`${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`, {
1168
- method: "GET",
1169
- headers: {
1170
- Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
1171
- },
1172
- });
1173
- if (!response.ok) {
1174
- console.error(`[CodexManager] Failed to fetch GitHub tokens: ${response.status}`);
1175
- return undefined;
1176
- }
1177
- const data = (await response.json());
1178
- return data.githubToken;
1179
- }
1180
- catch (error) {
1181
- console.error("[CodexManager] Error fetching GitHub tokens", error);
1182
- return undefined;
1183
- }
1184
- }
1185
- generateToolToken(context) {
1186
- if (!context.config.mcpServers)
1187
- return undefined;
1188
- const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
1189
- const runnerUid = this.runner.getRunnerUid();
1190
- if (!runnerToken || !runnerUid)
1191
- return undefined;
1192
- return jwt.sign({
1193
- conversationId: context.conversationId,
1194
- runnerUid,
1195
- }, runnerToken, {
1196
- expiresIn: "60m",
1197
- });
1198
- }
1199
- /**
1200
- * Detect whether incoming content contains multimodal blocks (image/document).
1201
- */
1202
- hasMultimodalContent(value) {
1203
- if (!Array.isArray(value))
1204
- return false;
1205
- return value.some((item) => item &&
1206
- typeof item === "object" &&
1207
- (item.type === "image" || item.type === "document"));
1208
- }
1209
- /**
1210
- * Normalize incoming content into the Codex SDK Input shape.
1211
- * - Text-only -> single string (lets SDK do fast path)
1212
- * - Multimodal -> [{ text }, ...{ local_image }]
1213
- */
1214
- async normalizeToInput(value, context, forceInstructions) {
1215
- if (Array.isArray(value) && this.hasMultimodalContent(value)) {
1216
- const inputs = [];
1217
- // Collect any text blocks (or document placeholders) to keep context
1218
- const textChunks = [];
1219
- for (const item of value) {
1220
- if (!item || typeof item !== "object")
1221
- continue;
1222
- if (item.type === "text" && typeof item.text === "string") {
1223
- textChunks.push(item.text);
1224
- }
1225
- else if (item.type === "document" && item.source) {
1226
- const docUrl = item.source.type === "url" ? item.source.url : item.source.data;
1227
- if (docUrl) {
1228
- textChunks.push(`[Document: ${docUrl}]`);
1229
- }
1230
- }
1231
- }
1232
- const textPart = this.buildPromptForMessage(context, textChunks.join(" ").trim(), forceInstructions);
1233
- if (textPart) {
1234
- inputs.push({ type: "text", text: textPart });
1235
- }
1236
- // Download image blocks to temp files and attach as local_image
1237
- const imagePaths = [];
1238
- for (const item of value) {
1239
- if (!item || typeof item !== "object" || item.type !== "image" || !item.source) {
1240
- continue;
1241
- }
1242
- const imageUrl = item.source.type === "url" ? item.source.url : item.source.data;
1243
- if (!imageUrl)
1244
- continue;
1245
- const localPath = await this.downloadImageToTemp(imageUrl);
1246
- inputs.push({ type: "local_image", path: localPath });
1247
- imagePaths.push(localPath);
1248
- }
1249
- // If no text and instructions were never injected, inject an empty prompt to carry instructions
1250
- if (!inputs.some((i) => i.type === "text")) {
1251
- const instructionOnly = this.buildPromptForMessage(context, "", forceInstructions);
1252
- if (instructionOnly) {
1253
- inputs.unshift({ type: "text", text: instructionOnly });
1254
- }
1255
- }
1256
- if (isDebugEnabledFor("manager") || isDebugEnabledFor("sdk")) {
1257
- console.log("[CodexManager] normalizeToInput multimodal", {
1258
- conversationId: context.conversationId,
1259
- imageCount: imagePaths.length,
1260
- imagePaths,
1261
- textIncluded: inputs.some((i) => i.type === "text"),
1262
- });
1263
- }
1264
- return inputs;
1265
- }
1266
- // Fallback to text-only path
1267
- const messageText = this.normalizeToText(value);
1268
- return this.buildPromptForMessage(context, messageText, forceInstructions);
1269
- }
1270
- /**
1271
- * Download an image (http(s) or data URL) to a temp file and return the path.
1272
- */
1273
- async downloadImageToTemp(url) {
1274
- // Allow local filesystem paths to be passed through unchanged
1275
- if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
1276
- return path.isAbsolute(url) ? url : path.resolve(url);
1277
- }
1278
- // Handle data URLs (e.g., data:image/png;base64,...)
1279
- if (url.startsWith("data:")) {
1280
- const commaIdx = url.indexOf(",");
1281
- if (commaIdx === -1) {
1282
- throw new Error("Invalid data URL for image");
1283
- }
1284
- const header = url.slice(5, commaIdx); // remove "data:"
1285
- const dataPart = url.slice(commaIdx + 1);
1286
- const isBase64 = header.endsWith(";base64");
1287
- const mime = header.replace(";base64", "");
1288
- const ext = this.mimeToExtension(mime) || "png";
1289
- const buffer = isBase64
1290
- ? Buffer.from(dataPart, "base64")
1291
- : Buffer.from(decodeURIComponent(dataPart), "utf-8");
1292
- return await this.writeBufferToTemp(buffer, ext);
1293
- }
1294
- // Standard remote URL
1295
- const response = await fetch(url);
1296
- if (!response.ok) {
1297
- throw new Error(`Failed to download image (${response.status}) from ${url}`);
1298
- }
1299
- const arrayBuffer = await response.arrayBuffer();
1300
- const buffer = Buffer.from(arrayBuffer);
1301
- const ext = this.inferExtensionFromUrl(url) || "png";
1302
- return await this.writeBufferToTemp(buffer, ext);
1303
- }
1304
- async writeBufferToTemp(buffer, ext) {
1305
- const dir = path.join(tmpdir(), "codex-images");
1306
- await fs.mkdir(dir, { recursive: true });
1307
- const filename = `${crypto.randomUUID()}.${ext}`;
1308
- const fullPath = path.join(dir, filename);
1309
- await fs.writeFile(fullPath, buffer);
1310
- return fullPath;
1311
- }
1312
- inferExtensionFromUrl(url) {
1313
- try {
1314
- const parsed = new URL(url);
1315
- const pathname = parsed.pathname;
1316
- const ext = path.extname(pathname);
1317
- if (ext) {
1318
- return ext.replace(".", "").split(/[?#]/)[0] || null;
1319
- }
1320
- }
1321
- catch {
1322
- // Not a valid URL; fall through
1323
- }
1324
- return null;
1325
- }
1326
- mimeToExtension(mime) {
1327
- if (!mime)
1328
- return null;
1329
- const map = {
1330
- "image/png": "png",
1331
- "image/jpeg": "jpg",
1332
- "image/jpg": "jpg",
1333
- "image/webp": "webp",
1334
- "image/gif": "gif",
1335
- };
1336
- return map[mime] || null;
1337
- }
1338
- normalizeToText(value) {
1339
- if (typeof value === "string")
1340
- return value;
1341
- if (value == null)
1342
- return "";
1343
- if (typeof value === "object") {
1344
- if (typeof value.text === "string") {
1345
- return value.text;
1346
- }
1347
- if (Array.isArray(value)) {
1348
- const texts = value
1349
- .map((entry) => {
1350
- if (entry &&
1351
- typeof entry === "object" &&
1352
- typeof entry.text === "string") {
1353
- return entry.text;
1354
- }
1355
- return null;
1356
- })
1357
- .filter((entry) => Boolean(entry));
1358
- if (texts.length) {
1359
- return texts.join(" ");
1360
- }
1361
- }
1362
- if (typeof value.content === "string") {
1363
- return value.content;
1364
- }
1365
- }
1366
- try {
1367
- return JSON.stringify(value);
1368
- }
1369
- catch {
1370
- return String(value);
1371
- }
1372
- }
1373
- normalizeSessionId(value) {
1374
- if (!value)
1375
- return undefined;
1376
- const trimmed = value.trim();
1377
- return trimmed.length ? trimmed : undefined;
1378
- }
1379
- resolveSessionId(config, conversationData, override) {
1380
- return (this.normalizeSessionId(override) ||
1381
- this.normalizeSessionId(config?.sessionId) ||
1382
- this.normalizeSessionId(conversationData?.agentSessionId) ||
1383
- this.normalizeSessionId(conversationData?.threadId) ||
1384
- this.normalizeSessionId(conversationData?.sessionId) ||
1385
- "");
1386
- }
1387
- isAbortError(error) {
1388
- if (!error)
1389
- return false;
1390
- return (error.name === "AbortError" ||
1391
- /aborted|abort/i.test(error.message || ""));
1392
- }
1393
- async _finalizeConversation(context, hadError, error, reason) {
1394
- // Synchronous idempotency check - must happen before any async operations
1395
- if (context._finalized)
1396
- return;
1397
- context._finalized = true;
1398
- // Mark as completed immediately to prevent restart on catch-up
1399
- // This is synchronous and happens before any async operations
1400
- this.runner.markConversationCompleted(context.conversationId);
1401
- // Clean up local state synchronously
1402
- this.threadStates.delete(context.conversationId);
1403
- this.runner.activeConversations_.delete(context.conversationId);
1404
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
1405
- // Now do async notification (after all sync cleanup)
1406
- try {
1407
- await this.runner.notify("conversation.end", {
1408
- conversationId: context.conversationId,
1409
- conversationObjectType: context.conversationObjectType,
1410
- conversationObjectId: context.conversationObjectId,
1411
- agentSessionId: context.agentSessionId,
1412
- isError: hadError,
1413
- errorMessage: error?.message,
1414
- reason,
1415
- });
1416
- }
1417
- catch (notifyError) {
1418
- console.error("[CodexManager] Failed to send conversation.end notification", notifyError);
1419
- }
1420
- // Notify update coordinator that a conversation has ended
1421
- // This may trigger an auto-update if one is pending and runner is now idle
1422
- try {
1423
- await this.runner.onConversationEnd();
1424
- }
1425
- catch (e) {
1426
- console.error("[CodexManager] Failed to notify onConversationEnd:", e);
1427
- }
1428
- }
1429
- async _handleConversationError(context, error) {
1430
- console.error(`[CodexManager] Conversation error for ${context.conversationId}:`, error);
1431
- await this.runner.notify("error.report", {
1432
- conversationId: context.conversationId,
1433
- conversationObjectType: context.conversationObjectType,
1434
- conversationObjectId: context.conversationObjectId,
1435
- agentSessionId: context.agentSessionId,
1436
- errorType: "codex_error",
1437
- message: error.message,
1438
- details: {
1439
- stack: error.stack,
1440
- timestamp: new Date(),
1441
- },
1442
- });
1443
- }
1444
- generateMessageId(context) {
1445
- return `${context.agentSessionId}-${Date.now()}-${Math.random()
1446
- .toString(36)
1447
- .slice(2)}`;
1448
- }
1449
- }
1450
- //# sourceMappingURL=codex-sdk-manager.js.map