@northflare/runner 0.0.12 → 0.0.13

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 (77) hide show
  1. package/package.json +2 -3
  2. package/coverage/base.css +0 -224
  3. package/coverage/block-navigation.js +0 -87
  4. package/coverage/coverage-final.json +0 -12
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +0 -176
  7. package/coverage/lib/index.html +0 -116
  8. package/coverage/lib/preload-script.js.html +0 -964
  9. package/coverage/prettify.css +0 -1
  10. package/coverage/prettify.js +0 -2
  11. package/coverage/sort-arrow-sprite.png +0 -0
  12. package/coverage/sorter.js +0 -196
  13. package/coverage/src/collections/index.html +0 -116
  14. package/coverage/src/collections/runner-messages.ts.html +0 -312
  15. package/coverage/src/components/claude-manager.ts.html +0 -1290
  16. package/coverage/src/components/index.html +0 -146
  17. package/coverage/src/components/message-handler.ts.html +0 -730
  18. package/coverage/src/components/repository-manager.ts.html +0 -841
  19. package/coverage/src/index.html +0 -131
  20. package/coverage/src/index.ts.html +0 -448
  21. package/coverage/src/runner.ts.html +0 -1239
  22. package/coverage/src/utils/config.ts.html +0 -780
  23. package/coverage/src/utils/console.ts.html +0 -121
  24. package/coverage/src/utils/index.html +0 -161
  25. package/coverage/src/utils/logger.ts.html +0 -475
  26. package/coverage/src/utils/status-line.ts.html +0 -445
  27. package/exceptions.log +0 -24
  28. package/lib/codex-sdk/src/codex.ts +0 -38
  29. package/lib/codex-sdk/src/codexOptions.ts +0 -10
  30. package/lib/codex-sdk/src/events.ts +0 -80
  31. package/lib/codex-sdk/src/exec.ts +0 -336
  32. package/lib/codex-sdk/src/index.ts +0 -39
  33. package/lib/codex-sdk/src/items.ts +0 -127
  34. package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
  35. package/lib/codex-sdk/src/thread.ts +0 -155
  36. package/lib/codex-sdk/src/threadOptions.ts +0 -18
  37. package/lib/codex-sdk/src/turnOptions.ts +0 -6
  38. package/lib/codex-sdk/tests/abort.test.ts +0 -165
  39. package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
  40. package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
  41. package/lib/codex-sdk/tests/run.test.ts +0 -687
  42. package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
  43. package/lib/codex-sdk/tsconfig.json +0 -24
  44. package/rejections.log +0 -68
  45. package/runner.log +0 -488
  46. package/src/components/claude-sdk-manager.ts +0 -1425
  47. package/src/components/codex-sdk-manager.ts +0 -1358
  48. package/src/components/enhanced-repository-manager.ts +0 -823
  49. package/src/components/message-handler-sse.ts +0 -1097
  50. package/src/components/repository-manager.ts +0 -337
  51. package/src/index.ts +0 -168
  52. package/src/runner-sse.ts +0 -917
  53. package/src/services/RunnerAPIClient.ts +0 -175
  54. package/src/services/SSEClient.ts +0 -258
  55. package/src/types/claude.ts +0 -66
  56. package/src/types/computer-name.d.ts +0 -4
  57. package/src/types/index.ts +0 -64
  58. package/src/types/messages.ts +0 -39
  59. package/src/types/runner-interface.ts +0 -36
  60. package/src/utils/StateManager.ts +0 -187
  61. package/src/utils/config.ts +0 -327
  62. package/src/utils/console.ts +0 -15
  63. package/src/utils/debug.ts +0 -18
  64. package/src/utils/expand-env.ts +0 -22
  65. package/src/utils/logger.ts +0 -134
  66. package/src/utils/model.ts +0 -29
  67. package/src/utils/status-line.ts +0 -122
  68. package/src/utils/tool-response-sanitizer.ts +0 -160
  69. package/test-debug.sh +0 -26
  70. package/tests/retry-strategies.test.ts +0 -410
  71. package/tests/sdk-integration.test.ts +0 -329
  72. package/tests/sdk-streaming.test.ts +0 -1180
  73. package/tests/setup.ts +0 -5
  74. package/tests/test-claude-manager.ts +0 -120
  75. package/tests/tool-response-sanitizer.test.ts +0 -63
  76. package/tsconfig.json +0 -36
  77. package/vitest.config.ts +0 -27
@@ -1,1358 +0,0 @@
1
- import type { Thread, ThreadEvent, ThreadItem } from "@northflare/codex-sdk";
2
- import { IRunnerApp } from "../types/runner-interface";
3
- import { EnhancedRepositoryManager } from "./enhanced-repository-manager";
4
- import { ConversationContext, ConversationConfig, Message } from "../types";
5
- import { statusLineManager } from "../utils/status-line";
6
- import { console } from "../utils/console";
7
- import { expandEnv } from "../utils/expand-env";
8
- import { parseModelValue } from "../utils/model";
9
- import * as jwt from "jsonwebtoken";
10
- import fs from "fs/promises";
11
- import path from "path";
12
- import { isRunnerDebugEnabled } from "../utils/debug";
13
-
14
- let codexSdkPromise: Promise<typeof import("@northflare/codex-sdk")> | null =
15
- null;
16
- // Use runtime dynamic import so the CommonJS build can load the ESM-only SDK.
17
- const dynamicImport = new Function(
18
- "specifier",
19
- "return import(specifier);"
20
- ) as (specifier: string) => Promise<unknown>;
21
-
22
- async function loadCodexSdk() {
23
- if (!codexSdkPromise) {
24
- codexSdkPromise = dynamicImport("@northflare/codex-sdk") as Promise<
25
- typeof import("@northflare/codex-sdk")
26
- >;
27
- }
28
- return codexSdkPromise;
29
- }
30
-
31
- type CodexThreadState = {
32
- thread: Thread;
33
- abortController: AbortController | null;
34
- runPromise: Promise<void> | null;
35
- };
36
-
37
- type ConversationDetails = {
38
- id: string;
39
- objectType: string;
40
- objectId: string;
41
- model: string;
42
- globalInstructions: string;
43
- workspaceInstructions: string;
44
- permissionsMode: string;
45
- agentSessionId?: string;
46
- workspaceId?: string;
47
- threadId?: string;
48
- };
49
-
50
- type NormalizedItemEvent = {
51
- type: string;
52
- content: any;
53
- subtype?: string;
54
- toolCalls?: any;
55
- isError?: boolean;
56
- };
57
-
58
- export class CodexManager {
59
- private runner: IRunnerApp;
60
- private repositoryManager: EnhancedRepositoryManager;
61
- private threadStates: Map<string, CodexThreadState> = new Map();
62
-
63
- constructor(
64
- runner: IRunnerApp,
65
- repositoryManager: EnhancedRepositoryManager
66
- ) {
67
- this.runner = runner;
68
- this.repositoryManager = repositoryManager;
69
- }
70
-
71
- async startConversation(
72
- conversationObjectType: "Task" | "TaskPlan",
73
- conversationObjectId: string,
74
- config: ConversationConfig,
75
- initialMessages: Message[],
76
- conversationData?: ConversationDetails
77
- ): Promise<ConversationContext> {
78
- if (!conversationData?.id) {
79
- throw new Error(
80
- "startConversation requires conversationData with a valid conversation.id"
81
- );
82
- }
83
-
84
- const conversationId = conversationData.id;
85
- const agentSessionId = this.resolveSessionId(config, conversationData);
86
-
87
- const rawModel =
88
- conversationData?.model ||
89
- (config as any)?.model ||
90
- (config as any)?.defaultModel ||
91
- "openai";
92
- const { baseModel, reasoningEffort } = parseModelValue(rawModel);
93
- const normalizedModel = baseModel || rawModel;
94
-
95
- const metadata: Record<string, any> = {
96
- instructionsInjected: false,
97
- originalModelValue: rawModel,
98
- };
99
-
100
- if (reasoningEffort) {
101
- metadata["modelReasoningEffort"] = reasoningEffort;
102
- }
103
-
104
- const context: ConversationContext = {
105
- conversationId,
106
- agentSessionId,
107
- conversationObjectType,
108
- conversationObjectId,
109
- taskId:
110
- conversationObjectType === "Task" ? conversationObjectId : undefined,
111
- workspaceId: config.workspaceId,
112
- status: "starting",
113
- config,
114
- startedAt: new Date(),
115
- lastActivityAt: new Date(),
116
- model: normalizedModel,
117
- globalInstructions: conversationData.globalInstructions || "",
118
- workspaceInstructions: conversationData.workspaceInstructions || "",
119
- permissionsMode:
120
- conversationData.permissionsMode ||
121
- (config as any)?.permissionsMode ||
122
- "all",
123
- provider: "openai",
124
- metadata,
125
- };
126
-
127
- this.runner.activeConversations_.set(conversationId, context);
128
-
129
- console.log(`[CodexManager] Stored conversation context`, {
130
- conversationId,
131
- agentSessionId,
132
- model: context.model,
133
- permissionsMode: context.permissionsMode,
134
- });
135
-
136
- const workspacePath = await this.resolveWorkspacePath(context, config);
137
-
138
- (context.metadata as Record<string, any>)["workspacePath"] = workspacePath;
139
-
140
- const githubToken = context.workspaceId
141
- ? await this.fetchGithubTokens(context.workspaceId)
142
- : undefined;
143
-
144
- const toolToken = this.generateToolToken(context);
145
-
146
- let mcpServers: Record<string, any> | undefined;
147
- if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
148
- const expandedServers = expandEnv(config.mcpServers, {
149
- TOOL_TOKEN: toolToken,
150
- });
151
- mcpServers = this.normalizeMcpServersForCodex(expandedServers);
152
- console.log(
153
- "[CodexManager] MCP servers configuration:",
154
- JSON.stringify(mcpServers, null, 2)
155
- );
156
- (context.metadata as Record<string, any>)["mcpServers"] = mcpServers;
157
- }
158
-
159
- const configOverrides = this.buildConfigOverridesFromMcp(mcpServers);
160
-
161
- const envVars = await this.buildEnvVars(config, githubToken, toolToken);
162
-
163
- const { Codex } = await loadCodexSdk();
164
- const codex = new Codex({
165
- baseUrl:
166
- (config as any)?.openaiBaseUrl ||
167
- (config as any)?.codexBaseUrl ||
168
- process.env["OPENAI_BASE_URL"],
169
- apiKey:
170
- (config as any)?.openaiApiKey ||
171
- (config as any)?.codexApiKey ||
172
- process.env["CODEX_API_KEY"] ||
173
- process.env["OPENAI_API_KEY"],
174
- env: envVars,
175
- });
176
-
177
- const threadOptions = {
178
- model: context.model,
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: reasoningEffort,
187
- } as const;
188
-
189
- console.log(
190
- "[CodexManager] Thread options:",
191
- JSON.stringify(threadOptions, null, 2)
192
- );
193
-
194
- const thread = agentSessionId
195
- ? codex.resumeThread(agentSessionId, threadOptions)
196
- : codex.startThread(threadOptions);
197
-
198
- this.threadStates.set(conversationId, {
199
- thread,
200
- abortController: null,
201
- runPromise: null,
202
- });
203
-
204
- context.conversation = thread;
205
-
206
- // Send initial messages sequentially without waiting for completion
207
- if (initialMessages?.length) {
208
- for (const message of initialMessages) {
209
- const text = this.normalizeToText(message.content);
210
- const prompt = this.buildPromptForMessage(context, text, true);
211
- this.launchTurn(context, prompt);
212
- }
213
- }
214
-
215
- return context;
216
- }
217
-
218
- async stopConversation(
219
- _agentSessionId: string,
220
- context: ConversationContext,
221
- isRunnerShutdown: boolean = false,
222
- reason?: string
223
- ): Promise<void> {
224
- context.status = "stopping";
225
-
226
- await this.abortActiveRun(context.conversationId);
227
-
228
- try {
229
- await this._finalizeConversation(
230
- context,
231
- false,
232
- undefined,
233
- reason || (isRunnerShutdown ? "runner_shutdown" : undefined)
234
- );
235
- } catch (error) {
236
- console.error(
237
- `[CodexManager] Error finalizing conversation ${context.conversationId}:`,
238
- error
239
- );
240
- } finally {
241
- this.threadStates.delete(context.conversationId);
242
- }
243
-
244
- context.status = "stopped";
245
- }
246
-
247
- async resumeConversation(
248
- conversationObjectType: "Task" | "TaskPlan",
249
- conversationObjectId: string,
250
- agentSessionId: string,
251
- config: ConversationConfig,
252
- conversationData?: ConversationDetails,
253
- resumeMessage?: string
254
- ): Promise<string> {
255
- console.log(`[CodexManager] Resuming conversation ${agentSessionId}`);
256
-
257
- const context = await this.startConversation(
258
- conversationObjectType,
259
- conversationObjectId,
260
- { ...config, sessionId: agentSessionId },
261
- [],
262
- conversationData
263
- );
264
-
265
- if (resumeMessage) {
266
- const prompt = this.buildPromptForMessage(context, resumeMessage, false);
267
- this.launchTurn(context, prompt);
268
- }
269
-
270
- return context.agentSessionId;
271
- }
272
-
273
- async sendUserMessage(
274
- conversationId: string,
275
- content: any,
276
- config?: ConversationConfig,
277
- conversationObjectType?: "Task" | "TaskPlan",
278
- conversationObjectId?: string,
279
- conversation?: any,
280
- agentSessionIdOverride?: string
281
- ): Promise<void> {
282
- console.log(`[CodexManager] sendUserMessage called`, {
283
- conversationId,
284
- hasConfig: !!config,
285
- });
286
-
287
- let context = this.runner.getConversationContext(conversationId);
288
-
289
- if (!context && conversation) {
290
- const conversationDetails = conversation as ConversationDetails;
291
- const resumeSessionId = this.resolveSessionId(
292
- config,
293
- conversationDetails,
294
- agentSessionIdOverride
295
- );
296
- const startConfig: ConversationConfig = {
297
- ...(config || {}),
298
- workspaceId:
299
- conversationDetails.workspaceId || config?.workspaceId || undefined,
300
- ...(resumeSessionId ? { sessionId: resumeSessionId } : {}),
301
- };
302
-
303
- context = await this.startConversation(
304
- (conversationObjectType as "Task" | "TaskPlan") ||
305
- (conversationDetails.objectType as "Task" | "TaskPlan"),
306
- conversationObjectId || conversationDetails.objectId,
307
- startConfig,
308
- [],
309
- conversationDetails
310
- );
311
- }
312
-
313
- if (!context) {
314
- throw new Error(
315
- `No active or fetchable conversation found for ${conversationId}`
316
- );
317
- }
318
-
319
- await this.abortActiveRun(conversationId);
320
-
321
- const messageText = this.normalizeToText(content);
322
- const prompt = this.buildPromptForMessage(context, messageText, false);
323
- this.launchTurn(context, prompt);
324
-
325
- context.lastActivityAt = new Date();
326
- }
327
-
328
- private async buildEnvVars(
329
- config: ConversationConfig,
330
- githubToken?: string,
331
- toolToken?: string
332
- ): Promise<Record<string, string>> {
333
- const envVars: Record<string, string> = {
334
- ...(Object.fromEntries(
335
- Object.entries(process.env).filter(([, value]) => value !== undefined)
336
- ) as Record<string, string>),
337
- };
338
-
339
- if (config.codexAuth?.accessToken) {
340
- envVars["OPENAI_ACCESS_TOKEN"] = config.codexAuth.accessToken;
341
- } else if (config.accessToken) {
342
- envVars["OPENAI_ACCESS_TOKEN"] = config.accessToken;
343
- }
344
-
345
- if (githubToken) {
346
- envVars["GITHUB_TOKEN"] = githubToken;
347
- }
348
-
349
- if (toolToken) {
350
- envVars["TOOL_TOKEN"] = toolToken;
351
- }
352
-
353
- if (isRunnerDebugEnabled()) {
354
- envVars["DEBUG"] = "1";
355
- }
356
-
357
- const codexHome = await this.ensureCodexAuthHome(config);
358
- if (codexHome) {
359
- envVars["CODEX_HOME"] = codexHome;
360
- }
361
-
362
- return envVars;
363
- }
364
-
365
- private async ensureCodexAuthHome(
366
- config: ConversationConfig
367
- ): Promise<string | null> {
368
- if (!config.codexAuth) {
369
- return null;
370
- }
371
-
372
- const runnerId = this.runner.getRunnerId();
373
- const dataDir = this.runner.config_.dataDir;
374
-
375
- if (!runnerId || !dataDir) {
376
- console.warn(
377
- "[CodexManager] Missing runnerId or dataDir; cannot prepare Codex auth directory"
378
- );
379
- return null;
380
- }
381
-
382
- const codexDir = path.join(dataDir, "codex", runnerId);
383
- const authPayload = {
384
- OPENAI_API_KEY: null,
385
- tokens: {
386
- id_token: config.codexAuth.idToken,
387
- access_token: config.codexAuth.accessToken,
388
- refresh_token: "",
389
- account_id: config.codexAuth.accountId,
390
- },
391
- last_refresh: config.codexAuth.lastRefresh || new Date().toISOString(),
392
- };
393
-
394
- try {
395
- await fs.mkdir(codexDir, { recursive: true });
396
- await fs.writeFile(
397
- path.join(codexDir, "auth.json"),
398
- JSON.stringify(authPayload, null, 2),
399
- "utf-8"
400
- );
401
- return codexDir;
402
- } catch (error) {
403
- console.error(
404
- "[CodexManager] Failed to persist Codex auth configuration:",
405
- error
406
- );
407
- throw new Error(
408
- "Runner failed to persist Codex credentials. Check runner logs for details."
409
- );
410
- }
411
- }
412
-
413
- private buildPromptForMessage(
414
- context: ConversationContext,
415
- text: string,
416
- forceInstructions: boolean
417
- ): string {
418
- const trimmed = text ?? "";
419
- const instructions = this.getInstructionPrefix(context);
420
-
421
- const metadata =
422
- (context.metadata as Record<string, any>) || ({} as Record<string, any>);
423
- const shouldInjectInstructions =
424
- forceInstructions || !metadata["instructionsInjected"];
425
-
426
- if (instructions && shouldInjectInstructions) {
427
- metadata["instructionsInjected"] = true;
428
- context.metadata = metadata;
429
- return `${instructions}\n\n${trimmed}`.trim();
430
- }
431
-
432
- return trimmed;
433
- }
434
-
435
- private getInstructionPrefix(context: ConversationContext): string {
436
- const parts: string[] = [];
437
-
438
- if (context.globalInstructions) {
439
- parts.push(
440
- `<global-instructions>\n${context.globalInstructions}\n</global-instructions>`
441
- );
442
- }
443
-
444
- if (context.workspaceInstructions) {
445
- parts.push(
446
- `<workspace-instructions>\n${context.workspaceInstructions}\n</workspace-instructions>`
447
- );
448
- }
449
-
450
- return parts.join("\n\n");
451
- }
452
-
453
- private launchTurn(context: ConversationContext, prompt: string): void {
454
- const state = this.threadStates.get(context.conversationId);
455
-
456
- if (!state || !state.thread) {
457
- throw new Error(
458
- `Thread state missing for conversation ${context.conversationId}`
459
- );
460
- }
461
-
462
- const abortController = new AbortController();
463
- state.abortController = abortController;
464
-
465
- context.lastActivityAt = new Date();
466
-
467
- const runPromise = this.streamThreadEvents(
468
- context,
469
- state.thread,
470
- prompt,
471
- abortController
472
- );
473
-
474
- state.runPromise = runPromise;
475
-
476
- runPromise
477
- .catch((error) => {
478
- if (!this.isAbortError(error)) {
479
- console.error(
480
- `[CodexManager] Run failed for ${context.conversationId}:`,
481
- error
482
- );
483
- this._handleConversationError(context, error as Error);
484
- }
485
- })
486
- .finally(() => {
487
- if (state.runPromise === runPromise) {
488
- state.runPromise = null;
489
- }
490
- if (state.abortController === abortController) {
491
- state.abortController = null;
492
- }
493
- });
494
- }
495
-
496
- private async streamThreadEvents(
497
- context: ConversationContext,
498
- thread: Thread,
499
- prompt: string,
500
- abortController: AbortController
501
- ): Promise<void> {
502
- try {
503
- const { events } = await thread.runStreamed(prompt, {
504
- signal: abortController.signal,
505
- });
506
-
507
- for await (const event of events) {
508
- await this.handleThreadEvent(context, event);
509
- }
510
- } catch (error) {
511
- if (this.isAbortError(error)) {
512
- console.log(
513
- `[CodexManager] Turn aborted for ${context.conversationId}`
514
- );
515
- return;
516
- }
517
-
518
- throw error;
519
- }
520
- }
521
-
522
- private async abortActiveRun(conversationId: string): Promise<void> {
523
- const state = this.threadStates.get(conversationId);
524
- if (!state || !state.runPromise) return;
525
-
526
- if (state.abortController) {
527
- state.abortController.abort();
528
- }
529
-
530
- try {
531
- await state.runPromise;
532
- } catch (error) {
533
- if (!this.isAbortError(error)) {
534
- console.warn(
535
- `[CodexManager] Run aborted with error for ${conversationId}:`,
536
- error
537
- );
538
- }
539
- }
540
- }
541
-
542
- private async handleThreadEvent(
543
- context: ConversationContext,
544
- event: ThreadEvent
545
- ): Promise<void> {
546
- try {
547
- this.logRawThreadEvent(event);
548
-
549
- switch (event.type) {
550
- case "thread.started": {
551
- await this.handleThreadStarted(context, event.thread_id);
552
- break;
553
- }
554
- case "turn.started": {
555
- context.status = "active";
556
- await this.sendAgentMessage(context, "system", {
557
- subtype: "turn.started",
558
- content: [
559
- {
560
- type: "text",
561
- text: `Turn started at ${new Date().toISOString()}`,
562
- },
563
- ],
564
- });
565
- break;
566
- }
567
- case "turn.completed": {
568
- await this.sendAgentMessage(context, "result", {
569
- subtype: "turn.completed",
570
- content: [
571
- {
572
- type: "usage",
573
- usage: event.usage,
574
- },
575
- ],
576
- });
577
- context.status = "stopped";
578
- await this._finalizeConversation(context, false);
579
- break;
580
- }
581
- case "turn.failed": {
582
- const error = new Error(event.error?.message || "Turn failed");
583
- await this.sendAgentMessage(context, "error", {
584
- subtype: "turn.failed",
585
- content: [
586
- {
587
- type: "text",
588
- text: error.message,
589
- },
590
- ],
591
- isError: true,
592
- });
593
- await this._handleConversationError(context, error);
594
- context.status = "stopped";
595
- await this._finalizeConversation(context, true, error, "turn_failed");
596
- break;
597
- }
598
- case "item.started":
599
- case "item.updated":
600
- case "item.completed": {
601
- await this.forwardItemEvent(
602
- context,
603
- event.item,
604
- event.type.split(".")[1] as "started" | "updated" | "completed"
605
- );
606
- break;
607
- }
608
- case "error": {
609
- const fatalError = new Error(event.message || "Unknown error");
610
- await this.sendAgentMessage(context, "error", {
611
- subtype: "thread.error",
612
- content: [{ type: "text", text: fatalError.message }],
613
- isError: true,
614
- });
615
- await this._handleConversationError(context, fatalError);
616
- context.status = "stopped";
617
- await this._finalizeConversation(
618
- context,
619
- true,
620
- fatalError,
621
- "thread_error"
622
- );
623
- break;
624
- }
625
- }
626
- } catch (error) {
627
- console.error("[CodexManager] Failed to handle thread event", {
628
- event,
629
- error,
630
- });
631
- }
632
- }
633
-
634
- private async forwardItemEvent(
635
- context: ConversationContext,
636
- item: ThreadItem,
637
- phase: "started" | "updated" | "completed"
638
- ): Promise<void> {
639
- const normalized = this.normalizeItemEvent(context, item, phase);
640
- if (!normalized) return;
641
-
642
- const { subtype, content, isError, toolCalls, type } = normalized;
643
- const payload: {
644
- subtype?: string;
645
- content: any;
646
- toolCalls?: any;
647
- isError?: boolean;
648
- } = {
649
- subtype,
650
- content,
651
- isError,
652
- };
653
-
654
- if (toolCalls) {
655
- payload.toolCalls = toolCalls;
656
- }
657
-
658
- await this.sendAgentMessage(context, type, payload);
659
- }
660
-
661
- private normalizeItemEvent(
662
- context: ConversationContext,
663
- item: ThreadItem,
664
- phase: "started" | "updated" | "completed"
665
- ): NormalizedItemEvent | null {
666
- switch (item.type) {
667
- case "agent_message": {
668
- if (phase !== "completed") return null;
669
- return {
670
- type: "assistant",
671
- content: [
672
- {
673
- type: "text",
674
- text: item.text || "",
675
- },
676
- ],
677
- };
678
- }
679
- case "reasoning": {
680
- return {
681
- type: "thinking",
682
- content: [
683
- {
684
- type: "thinking",
685
- thinking: item.text,
686
- text: item.text,
687
- phase,
688
- },
689
- ],
690
- };
691
- }
692
- case "command_execution": {
693
- // Namespace command_execution tool use IDs so they don't collide
694
- // with MCP tool call IDs that may reuse the same raw item.id.
695
- const internalNamespace = "codex_command";
696
- const toolUseId = this.buildToolUseId(
697
- context,
698
- `${internalNamespace}:${item.id}`
699
- );
700
- const timestamp = new Date().toISOString();
701
- const isTerminal =
702
- phase === "completed" ||
703
- item.status === "completed" ||
704
- item.status === "failed";
705
-
706
- if (!isTerminal) {
707
- if (phase !== "started") {
708
- return null;
709
- }
710
-
711
- return {
712
- type: "assistant",
713
- subtype: "tool_use",
714
- content: [
715
- {
716
- toolCalls: [
717
- {
718
- id: toolUseId,
719
- name: "codex_command",
720
- arguments: {
721
- command: item.command,
722
- status: item.status,
723
- },
724
- status: item.status,
725
- },
726
- ],
727
- timestamp,
728
- },
729
- ],
730
- };
731
- }
732
-
733
- const exitCode =
734
- typeof item.exit_code === "number" ? item.exit_code : null;
735
- const isError = item.status === "failed" || (exitCode ?? 0) !== 0;
736
-
737
- return {
738
- type: "tool_result",
739
- subtype: "tool_result",
740
- content: [
741
- {
742
- type: "tool_result",
743
- tool_use_id: toolUseId,
744
- content: {
745
- kind: "codex_command_result",
746
- command: item.command,
747
- output: item.aggregated_output || "",
748
- exitCode,
749
- status: item.status,
750
- },
751
- timestamp,
752
- },
753
- ],
754
- isError,
755
- };
756
- }
757
- case "file_change": {
758
- if (phase === "updated") return null;
759
- return {
760
- type: "system",
761
- subtype: "file_change",
762
- content: [
763
- {
764
- type: "file_change",
765
- changes: item.changes,
766
- status: item.status,
767
- },
768
- ],
769
- };
770
- }
771
- case "mcp_tool_call": {
772
- const toolName = this.buildMcpToolName(item.server, item.tool);
773
- // Namespace MCP tool call IDs with the MCP tool name so they don't
774
- // collide with internal tool IDs or MCP calls from other servers.
775
- const namespacedRawId = `mcp:${toolName}:${item.id}`;
776
- const toolUseId = this.buildToolUseId(context, namespacedRawId);
777
-
778
- if (item.status === "in_progress" || phase !== "completed") {
779
- return {
780
- type: "assistant",
781
- subtype: "tool_use",
782
- content: [
783
- {
784
- toolCalls: [
785
- {
786
- id: toolUseId,
787
- name: toolName,
788
- arguments: item.arguments,
789
- server: item.server,
790
- tool: item.tool,
791
- status: item.status,
792
- },
793
- ],
794
- timestamp: new Date().toISOString(),
795
- },
796
- ],
797
- };
798
- }
799
-
800
- if (item.status === "failed") {
801
- return {
802
- type: "error",
803
- subtype: "tool_result",
804
- content: [
805
- {
806
- type: "text",
807
- text: item.error?.message || "Tool call failed",
808
- tool_use_id: toolUseId,
809
- tool_name: toolName,
810
- timestamp: new Date().toISOString(),
811
- },
812
- ],
813
- isError: true,
814
- };
815
- }
816
-
817
- return {
818
- type: "tool_result",
819
- subtype: "tool_result",
820
- content: [
821
- {
822
- type: "tool_result",
823
- subtype: "tool_result",
824
- tool_use_id: toolUseId,
825
- content: item.result?.content || [],
826
- structured_content: item.result?.structured_content,
827
- metadata: {
828
- server: item.server,
829
- tool: item.tool,
830
- name: toolName,
831
- original_tool_use_id: item.id,
832
- },
833
- timestamp: new Date().toISOString(),
834
- },
835
- ],
836
- };
837
- }
838
- case "web_search": {
839
- return {
840
- type: "system",
841
- subtype: `web_search.${phase}`,
842
- content: [
843
- {
844
- type: "web_search",
845
- query: item.query,
846
- status: phase,
847
- },
848
- ],
849
- };
850
- }
851
- case "todo_list": {
852
- return {
853
- type: "system",
854
- subtype: "todo_list",
855
- content: [
856
- {
857
- type: "todo_list",
858
- items: item.items,
859
- phase,
860
- },
861
- ],
862
- };
863
- }
864
- case "error": {
865
- return {
866
- type: "error",
867
- subtype: `item.${phase}`,
868
- content: [
869
- {
870
- type: "text",
871
- text: item.message,
872
- },
873
- ],
874
- isError: true,
875
- };
876
- }
877
- default:
878
- return null;
879
- }
880
- }
881
-
882
- private async sendAgentMessage(
883
- context: ConversationContext,
884
- type: string,
885
- {
886
- subtype,
887
- content,
888
- toolCalls,
889
- isError,
890
- }: {
891
- subtype?: string;
892
- content: any;
893
- toolCalls?: any;
894
- isError?: boolean;
895
- }
896
- ): Promise<void> {
897
- const normalizedContent = Array.isArray(content)
898
- ? content
899
- : content
900
- ? [content]
901
- : [];
902
-
903
- const payload = {
904
- taskId: context.taskId,
905
- conversationId: context.conversationId,
906
- conversationObjectType: context.conversationObjectType,
907
- conversationObjectId: context.conversationObjectId,
908
- agentSessionId: context.agentSessionId,
909
- type,
910
- subtype,
911
- content: normalizedContent,
912
- toolCalls,
913
- isError: Boolean(isError),
914
- messageId: this.generateMessageId(context),
915
- timestamp: new Date().toISOString(),
916
- };
917
-
918
- if (isRunnerDebugEnabled()) {
919
- console.log("[CodexManager] Sending message.agent", payload);
920
- }
921
-
922
- await this.runner.notify("message.agent", payload);
923
- }
924
-
925
- private async handleThreadStarted(
926
- context: ConversationContext,
927
- threadId: string
928
- ): Promise<void> {
929
- if (!threadId || threadId === context.agentSessionId) {
930
- return;
931
- }
932
-
933
- const oldSessionId = context.agentSessionId;
934
- context.agentSessionId = threadId;
935
-
936
- await this.runner.notify("agentSessionId.changed", {
937
- conversationId: context.conversationId,
938
- conversationObjectType: context.conversationObjectType,
939
- conversationObjectId: context.conversationObjectId,
940
- oldAgentSessionId: oldSessionId,
941
- newAgentSessionId: threadId,
942
- });
943
- }
944
-
945
- private mapSandboxMode(
946
- permissionsMode?: string
947
- ): "read-only" | "workspace-write" | "danger-full-access" {
948
- const mode = (permissionsMode || "").toLowerCase();
949
- if (mode === "read" || mode === "read_only") {
950
- return "read-only";
951
- }
952
- if (mode === "workspace" || mode === "workspace-write") {
953
- return "workspace-write";
954
- }
955
- return "danger-full-access";
956
- }
957
-
958
- private shouldEnableNetwork(permissionsMode?: string): boolean {
959
- const mode = (permissionsMode || "").toLowerCase();
960
- return mode !== "read" && mode !== "read_only";
961
- }
962
-
963
- private getAdditionalDirectories(
964
- config: ConversationConfig
965
- ): string[] | undefined {
966
- const additionalDirs: string[] = [];
967
- if (config.runnerRepoPath) {
968
- additionalDirs.push(config.runnerRepoPath);
969
- }
970
- return additionalDirs.length ? additionalDirs : undefined;
971
- }
972
-
973
- private buildConfigOverridesFromMcp(
974
- mcpServers?: Record<string, any>
975
- ): Record<string, unknown> | undefined {
976
- if (!mcpServers) return undefined;
977
- const overrides: Record<string, unknown> = {};
978
-
979
- for (const [serverName, config] of Object.entries(mcpServers)) {
980
- this.flattenOverrideObject(
981
- `mcp_servers.${serverName}`,
982
- config,
983
- overrides
984
- );
985
- }
986
-
987
- return Object.keys(overrides).length ? overrides : undefined;
988
- }
989
-
990
- private normalizeMcpServersForCodex(
991
- mcpServers: Record<string, any>
992
- ): Record<string, any> {
993
- const normalized: Record<string, any> = {};
994
- for (const [serverName, config] of Object.entries(mcpServers)) {
995
- normalized[serverName] = this.stripAuthorizationHeader(config);
996
- }
997
- return normalized;
998
- }
999
-
1000
- private buildMcpToolName(server?: string, tool?: string): string {
1001
- const safeServer = (server || "unknown").trim() || "unknown";
1002
- const safeTool = (tool || "unknown").trim() || "unknown";
1003
- return `mcp__${safeServer}__${safeTool}`;
1004
- }
1005
-
1006
- private logRawThreadEvent(event: ThreadEvent): void {
1007
- if (!isRunnerDebugEnabled()) {
1008
- return;
1009
- }
1010
-
1011
- try {
1012
- console.log(
1013
- "[CodexManager] RAW Codex event FULL:",
1014
- this.safeStringify(event)
1015
- );
1016
-
1017
- const summary: Record<string, unknown> = {
1018
- type: event?.type,
1019
- keys: Object.keys((event as Record<string, unknown>) || {}),
1020
- hasItem: Boolean((event as any)?.item),
1021
- itemType: (event as any)?.item?.type,
1022
- hasUsage: Boolean((event as any)?.usage),
1023
- threadId: (event as any)?.thread_id,
1024
- turnId: (event as any)?.turn_id,
1025
- };
1026
-
1027
- console.log("[CodexManager] RAW Codex event summary:", summary);
1028
-
1029
- if ((event as any)?.item) {
1030
- console.log(
1031
- "[CodexManager] RAW Codex event item:",
1032
- this.safeStringify((event as any).item)
1033
- );
1034
- }
1035
-
1036
- if ((event as any)?.usage) {
1037
- console.log("[CodexManager] RAW Codex usage:", (event as any).usage);
1038
- }
1039
- } catch (error) {
1040
- console.warn("[CodexManager] Failed to log raw Codex event:", error);
1041
- }
1042
- }
1043
-
1044
- private safeStringify(value: any): string {
1045
- const seen = new WeakSet();
1046
- return JSON.stringify(
1047
- value,
1048
- (key, nested) => {
1049
- if (typeof nested === "function") return undefined;
1050
- if (typeof nested === "bigint") return nested.toString();
1051
- if (typeof nested === "object" && nested !== null) {
1052
- if (seen.has(nested)) return "[Circular]";
1053
- seen.add(nested);
1054
- }
1055
- return nested;
1056
- },
1057
- 2
1058
- );
1059
- }
1060
-
1061
- private buildToolUseId(context: ConversationContext, rawId: string): string {
1062
- const scope =
1063
- context.agentSessionId ||
1064
- context.conversationId ||
1065
- context.conversationObjectId ||
1066
- "codex";
1067
- return `${scope}:${rawId}`;
1068
- }
1069
-
1070
- private stripAuthorizationHeader(config: any): any {
1071
- if (!config || typeof config !== "object" || Array.isArray(config)) {
1072
- return config;
1073
- }
1074
-
1075
- const normalized = { ...config };
1076
- if (
1077
- normalized.bearer_token_env_var &&
1078
- normalized.headers &&
1079
- typeof normalized.headers === "object" &&
1080
- !Array.isArray(normalized.headers)
1081
- ) {
1082
- const headers = { ...normalized.headers };
1083
- delete headers["Authorization"];
1084
- if (Object.keys(headers).length === 0) {
1085
- delete normalized.headers;
1086
- } else {
1087
- normalized.headers = headers;
1088
- }
1089
- }
1090
-
1091
- return normalized;
1092
- }
1093
-
1094
- private flattenOverrideObject(
1095
- prefix: string,
1096
- value: any,
1097
- target: Record<string, unknown>
1098
- ): void {
1099
- if (value === undefined) {
1100
- return;
1101
- }
1102
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1103
- target[prefix] = value;
1104
- return;
1105
- }
1106
-
1107
- const entries = Object.entries(value);
1108
- if (!entries.length) {
1109
- target[prefix] = {};
1110
- return;
1111
- }
1112
-
1113
- for (const [key, nested] of entries) {
1114
- this.flattenOverrideObject(`${prefix}.${key}`, nested, target);
1115
- }
1116
- }
1117
-
1118
- private async resolveWorkspacePath(
1119
- context: ConversationContext,
1120
- config: ConversationConfig
1121
- ): Promise<string> {
1122
- let workspacePath: string;
1123
- const workspaceId = config.workspaceId;
1124
-
1125
- if (config.runnerRepoPath) {
1126
- workspacePath = config.runnerRepoPath;
1127
- console.log(`[CodexManager] Using local workspace path ${workspacePath}`);
1128
-
1129
- if (context.conversationObjectType === "Task") {
1130
- const taskHandle = await this.repositoryManager.createLocalTaskHandle(
1131
- context.conversationObjectId,
1132
- workspacePath
1133
- );
1134
- (context as any).taskHandle = taskHandle;
1135
- }
1136
- return workspacePath;
1137
- }
1138
-
1139
- if (
1140
- context.conversationObjectType === "Task" &&
1141
- config.repository &&
1142
- workspaceId
1143
- ) {
1144
- if (!config.repository.url) {
1145
- throw new Error("Repository URL is required for task conversations");
1146
- }
1147
-
1148
- const taskHandle = await this.repositoryManager.createTaskWorktree(
1149
- context.conversationObjectId,
1150
- workspaceId,
1151
- config.repository.url,
1152
- config.repository.branch,
1153
- config.githubToken
1154
- );
1155
- (context as any).taskHandle = taskHandle;
1156
- workspacePath = taskHandle.worktreePath;
1157
- return workspacePath;
1158
- }
1159
-
1160
- if (config.repository && workspaceId) {
1161
- if (config.repository.type === "local" && config.repository.localPath) {
1162
- workspacePath = config.repository.localPath;
1163
- return workspacePath;
1164
- }
1165
-
1166
- workspacePath = await this.repositoryManager.checkoutRepository(
1167
- workspaceId,
1168
- config.repository.url,
1169
- config.repository.branch,
1170
- config.githubToken
1171
- );
1172
- return workspacePath;
1173
- }
1174
-
1175
- if (workspaceId) {
1176
- workspacePath =
1177
- await this.repositoryManager.getWorkspacePath(workspaceId);
1178
- return workspacePath;
1179
- }
1180
-
1181
- return process.cwd();
1182
- }
1183
-
1184
- private async fetchGithubTokens(
1185
- workspaceId: string
1186
- ): Promise<string | undefined> {
1187
- try {
1188
- const response = await fetch(
1189
- `${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`,
1190
- {
1191
- method: "GET",
1192
- headers: {
1193
- Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
1194
- },
1195
- }
1196
- );
1197
-
1198
- if (!response.ok) {
1199
- console.error(
1200
- `[CodexManager] Failed to fetch GitHub tokens: ${response.status}`
1201
- );
1202
- return undefined;
1203
- }
1204
-
1205
- const data = (await response.json()) as { githubToken?: string };
1206
- return data.githubToken;
1207
- } catch (error) {
1208
- console.error("[CodexManager] Error fetching GitHub tokens", error);
1209
- return undefined;
1210
- }
1211
- }
1212
-
1213
- private generateToolToken(context: ConversationContext): string | undefined {
1214
- if (!context.config.mcpServers) return undefined;
1215
-
1216
- const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
1217
- const runnerUid = this.runner.getRunnerUid();
1218
-
1219
- if (!runnerToken || !runnerUid) return undefined;
1220
-
1221
- return jwt.sign(
1222
- {
1223
- conversationId: context.conversationId,
1224
- runnerUid,
1225
- },
1226
- runnerToken,
1227
- {
1228
- expiresIn: "60m",
1229
- }
1230
- );
1231
- }
1232
-
1233
- private normalizeToText(value: any): string {
1234
- if (typeof value === "string") return value;
1235
- if (value == null) return "";
1236
-
1237
- if (typeof value === "object") {
1238
- if (typeof (value as any).text === "string") {
1239
- return (value as any).text;
1240
- }
1241
- if (Array.isArray(value)) {
1242
- const texts = value
1243
- .map((entry) => {
1244
- if (
1245
- entry &&
1246
- typeof entry === "object" &&
1247
- typeof entry.text === "string"
1248
- ) {
1249
- return entry.text;
1250
- }
1251
- return null;
1252
- })
1253
- .filter((entry): entry is string => Boolean(entry));
1254
- if (texts.length) {
1255
- return texts.join(" ");
1256
- }
1257
- }
1258
- if (typeof (value as any).content === "string") {
1259
- return (value as any).content;
1260
- }
1261
- }
1262
-
1263
- try {
1264
- return JSON.stringify(value);
1265
- } catch {
1266
- return String(value);
1267
- }
1268
- }
1269
-
1270
- private normalizeSessionId(value?: string | null): string | undefined {
1271
- if (!value) return undefined;
1272
- const trimmed = value.trim();
1273
- return trimmed.length ? trimmed : undefined;
1274
- }
1275
-
1276
- private resolveSessionId(
1277
- config?: ConversationConfig,
1278
- conversationData?: ConversationDetails,
1279
- override?: string | null
1280
- ): string {
1281
- return (
1282
- this.normalizeSessionId(override) ||
1283
- this.normalizeSessionId(config?.sessionId) ||
1284
- this.normalizeSessionId(conversationData?.agentSessionId) ||
1285
- this.normalizeSessionId((conversationData as any)?.threadId) ||
1286
- this.normalizeSessionId((conversationData as any)?.sessionId) ||
1287
- ""
1288
- );
1289
- }
1290
-
1291
- private isAbortError(error: unknown): boolean {
1292
- if (!error) return false;
1293
- return (
1294
- (error as any).name === "AbortError" ||
1295
- /aborted|abort/i.test((error as any).message || "")
1296
- );
1297
- }
1298
-
1299
- private async _finalizeConversation(
1300
- context: ConversationContext,
1301
- hadError: boolean,
1302
- error?: any,
1303
- reason?: string
1304
- ): Promise<void> {
1305
- if ((context as any)._finalized) return;
1306
- (context as any)._finalized = true;
1307
-
1308
- try {
1309
- await this.runner.notify("conversation.end", {
1310
- conversationId: context.conversationId,
1311
- conversationObjectType: context.conversationObjectType,
1312
- conversationObjectId: context.conversationObjectId,
1313
- agentSessionId: context.agentSessionId,
1314
- isError: hadError,
1315
- errorMessage: error?.message,
1316
- reason,
1317
- });
1318
- } catch (notifyError) {
1319
- console.error(
1320
- "[CodexManager] Failed to send conversation.end notification",
1321
- notifyError
1322
- );
1323
- }
1324
-
1325
- this.threadStates.delete(context.conversationId);
1326
- this.runner.activeConversations_.delete(context.conversationId);
1327
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
1328
- }
1329
-
1330
- private async _handleConversationError(
1331
- context: ConversationContext,
1332
- error: Error
1333
- ): Promise<void> {
1334
- console.error(
1335
- `[CodexManager] Conversation error for ${context.conversationId}:`,
1336
- error
1337
- );
1338
-
1339
- await this.runner.notify("error.report", {
1340
- conversationId: context.conversationId,
1341
- conversationObjectType: context.conversationObjectType,
1342
- conversationObjectId: context.conversationObjectId,
1343
- agentSessionId: context.agentSessionId,
1344
- errorType: "codex_error",
1345
- message: error.message,
1346
- details: {
1347
- stack: error.stack,
1348
- timestamp: new Date(),
1349
- },
1350
- });
1351
- }
1352
-
1353
- private generateMessageId(context: ConversationContext): string {
1354
- return `${context.agentSessionId}-${Date.now()}-${Math.random()
1355
- .toString(36)
1356
- .slice(2)}`;
1357
- }
1358
- }