@northflare/runner 0.0.12 → 0.0.14

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 (80) hide show
  1. package/dist/utils/logger.d.ts.map +1 -1
  2. package/dist/utils/logger.js +5 -8
  3. package/dist/utils/logger.js.map +1 -1
  4. package/package.json +1 -2
  5. package/coverage/base.css +0 -224
  6. package/coverage/block-navigation.js +0 -87
  7. package/coverage/coverage-final.json +0 -12
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +0 -176
  10. package/coverage/lib/index.html +0 -116
  11. package/coverage/lib/preload-script.js.html +0 -964
  12. package/coverage/prettify.css +0 -1
  13. package/coverage/prettify.js +0 -2
  14. package/coverage/sort-arrow-sprite.png +0 -0
  15. package/coverage/sorter.js +0 -196
  16. package/coverage/src/collections/index.html +0 -116
  17. package/coverage/src/collections/runner-messages.ts.html +0 -312
  18. package/coverage/src/components/claude-manager.ts.html +0 -1290
  19. package/coverage/src/components/index.html +0 -146
  20. package/coverage/src/components/message-handler.ts.html +0 -730
  21. package/coverage/src/components/repository-manager.ts.html +0 -841
  22. package/coverage/src/index.html +0 -131
  23. package/coverage/src/index.ts.html +0 -448
  24. package/coverage/src/runner.ts.html +0 -1239
  25. package/coverage/src/utils/config.ts.html +0 -780
  26. package/coverage/src/utils/console.ts.html +0 -121
  27. package/coverage/src/utils/index.html +0 -161
  28. package/coverage/src/utils/logger.ts.html +0 -475
  29. package/coverage/src/utils/status-line.ts.html +0 -445
  30. package/exceptions.log +0 -24
  31. package/lib/codex-sdk/src/codex.ts +0 -38
  32. package/lib/codex-sdk/src/codexOptions.ts +0 -10
  33. package/lib/codex-sdk/src/events.ts +0 -80
  34. package/lib/codex-sdk/src/exec.ts +0 -336
  35. package/lib/codex-sdk/src/index.ts +0 -39
  36. package/lib/codex-sdk/src/items.ts +0 -127
  37. package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
  38. package/lib/codex-sdk/src/thread.ts +0 -155
  39. package/lib/codex-sdk/src/threadOptions.ts +0 -18
  40. package/lib/codex-sdk/src/turnOptions.ts +0 -6
  41. package/lib/codex-sdk/tests/abort.test.ts +0 -165
  42. package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
  43. package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
  44. package/lib/codex-sdk/tests/run.test.ts +0 -687
  45. package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
  46. package/lib/codex-sdk/tsconfig.json +0 -24
  47. package/rejections.log +0 -68
  48. package/runner.log +0 -488
  49. package/src/components/claude-sdk-manager.ts +0 -1425
  50. package/src/components/codex-sdk-manager.ts +0 -1358
  51. package/src/components/enhanced-repository-manager.ts +0 -823
  52. package/src/components/message-handler-sse.ts +0 -1097
  53. package/src/components/repository-manager.ts +0 -337
  54. package/src/index.ts +0 -168
  55. package/src/runner-sse.ts +0 -917
  56. package/src/services/RunnerAPIClient.ts +0 -175
  57. package/src/services/SSEClient.ts +0 -258
  58. package/src/types/claude.ts +0 -66
  59. package/src/types/computer-name.d.ts +0 -4
  60. package/src/types/index.ts +0 -64
  61. package/src/types/messages.ts +0 -39
  62. package/src/types/runner-interface.ts +0 -36
  63. package/src/utils/StateManager.ts +0 -187
  64. package/src/utils/config.ts +0 -327
  65. package/src/utils/console.ts +0 -15
  66. package/src/utils/debug.ts +0 -18
  67. package/src/utils/expand-env.ts +0 -22
  68. package/src/utils/logger.ts +0 -134
  69. package/src/utils/model.ts +0 -29
  70. package/src/utils/status-line.ts +0 -122
  71. package/src/utils/tool-response-sanitizer.ts +0 -160
  72. package/test-debug.sh +0 -26
  73. package/tests/retry-strategies.test.ts +0 -410
  74. package/tests/sdk-integration.test.ts +0 -329
  75. package/tests/sdk-streaming.test.ts +0 -1180
  76. package/tests/setup.ts +0 -5
  77. package/tests/test-claude-manager.ts +0 -120
  78. package/tests/tool-response-sanitizer.test.ts +0 -63
  79. package/tsconfig.json +0 -36
  80. 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
- }