circuschief 0.5.0 → 0.7.0

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 (169) hide show
  1. package/package.json +2 -1
  2. package/packages/server/src/agents/AgentGateway.js +36 -3
  3. package/packages/server/src/agents/BaseAgent.js +15 -1
  4. package/packages/server/src/agents/LoggingAgentWrapper.js +4 -0
  5. package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +9 -6
  6. package/packages/server/src/agents/adapters/CodexAdapter.js +262 -14
  7. package/packages/server/src/agents/adapters/codexCliRunner.js +185 -0
  8. package/packages/server/src/agents/adapters/codexEventMapper.js +235 -0
  9. package/packages/server/src/agents/types.js +1 -0
  10. package/packages/server/src/agents/vcr/VCRAgentAdapter.js +8 -0
  11. package/packages/server/src/api/agents.js +27 -0
  12. package/packages/server/src/api/canvas.js +20 -0
  13. package/packages/server/src/api/index.js +2 -0
  14. package/packages/server/src/api/projects-session-helpers.js +25 -0
  15. package/packages/server/src/api/projects.js +8 -0
  16. package/packages/server/src/api/providers.js +1 -0
  17. package/packages/server/src/api/sessions-draft.js +1 -0
  18. package/packages/server/src/api/sessions-messages.js +6 -0
  19. package/packages/server/src/api/settings.js +52 -4
  20. package/packages/server/src/db/ConversationRepository.js +16 -3
  21. package/packages/server/src/db/ProjectDefaultsRepository.js +47 -37
  22. package/packages/server/src/db/ProviderRepository.js +62 -6
  23. package/packages/server/src/db/SessionRepository.js +74 -14
  24. package/packages/server/src/db/SettingsRepository.js +44 -16
  25. package/packages/server/src/db/conversation-helpers.js +1 -0
  26. package/packages/server/src/db/migrations/conversationsMigrations.js +4 -0
  27. package/packages/server/src/db/migrations/index.js +4 -0
  28. package/packages/server/src/db/migrations/miscMigrations.js +53 -3
  29. package/packages/server/src/db/migrations/sessionsMigrations.js +6 -1
  30. package/packages/server/src/db/session-helpers.js +8 -0
  31. package/packages/server/src/schema.sql +9 -0
  32. package/packages/server/src/services/agentCallLogger.js +1 -1
  33. package/packages/server/src/services/codexSpawnHelper.js +37 -0
  34. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  35. package/packages/server/src/services/conversationContext.js +27 -0
  36. package/packages/server/src/services/draftSessionService.js +15 -2
  37. package/packages/server/src/services/kanbanTriggers.js +3 -0
  38. package/packages/server/src/services/providerTestService.js +115 -15
  39. package/packages/server/src/services/sessionAgentGuard.js +38 -0
  40. package/packages/server/src/services/sessionExecution.js +127 -33
  41. package/packages/server/src/services/sessionManager.js +45 -8
  42. package/packages/server/src/services/sessionPrompts.js +29 -0
  43. package/packages/server/src/services/sessionProvider.js +160 -41
  44. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  45. package/packages/server/src/services/streamEventHandler.js +16 -2
  46. package/packages/server/src/services/streamUsageHandler.js +6 -0
  47. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  48. package/packages/server/src/services/summaryModelClient.js +154 -0
  49. package/packages/server/src/services/summaryModelResolver.js +148 -0
  50. package/packages/server/src/services/summaryService.js +6 -4
  51. package/packages/server/src/services/templateTriggerService.js +2 -0
  52. package/packages/server/src/services/usageTracker.js +5 -1
  53. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  54. package/packages/shared/src/constants.js +1 -2
  55. package/packages/shared/src/contracts/projects.js +2 -0
  56. package/packages/shared/src/contracts/providers.js +24 -7
  57. package/packages/shared/src/contracts/sessions.js +1 -1
  58. package/packages/shared/src/index.js +1 -0
  59. package/packages/shared/src/types.js +28 -0
  60. package/packages/shared/src/utils.js +9 -17
  61. package/packages/web/dist/assets/ActiveSessionsView-UJsCILDL.js +1 -0
  62. package/packages/web/dist/assets/{AgentLogsView-DCF2WvP2.js → AgentLogsView-BGFPLjLa.js} +1 -1
  63. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +1 -0
  64. package/packages/web/dist/assets/{ArchiveConfirmModal-fgoEQhfq.js → ArchiveConfirmModal-OFaj_uX5.js} +1 -1
  65. package/packages/web/dist/assets/{CommandButtonDetailView-DAg07cDQ.js → CommandButtonDetailView-D8S258uP.js} +1 -1
  66. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +1 -0
  67. package/packages/web/dist/assets/{GeneralSettingsView-Cn9VI2du.js → GeneralSettingsView-DsHChEhv.js} +1 -1
  68. package/packages/web/dist/assets/{InputWithButton-BvboBGbz.js → InputWithButton-Ci15ox0a.js} +1 -1
  69. package/packages/web/dist/assets/{InterpolationHelp-0GoSBPgf.js → InterpolationHelp-CIkOSkWX.js} +1 -1
  70. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +2 -0
  71. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +1 -0
  72. package/packages/web/dist/assets/{ModelSelector-DPPD-92R.css → ModelSelector-D8hbTRIt.css} +1 -1
  73. package/packages/web/dist/assets/{NewSessionView-C77YVqgY.js → NewSessionView-BCqtIgWH.js} +2 -2
  74. package/packages/web/dist/assets/{NewSessionView-D_Hi7M9g.css → NewSessionView-CUUdHkfv.css} +1 -1
  75. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +1 -0
  76. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +1 -0
  77. package/packages/web/dist/assets/{ProjectListView-CLwtuJ0J.js → ProjectListView-B9FuWESY.js} +1 -1
  78. package/packages/web/dist/assets/{ProjectNewView-CzDtVibO.js → ProjectNewView-D62jYlBL.js} +1 -1
  79. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +1 -0
  80. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +1 -0
  81. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +1 -0
  82. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  83. package/packages/web/dist/assets/{QuickResponsesPanel-CTXYjMF-.js → QuickResponsesPanel-DZ_Lre_l.js} +1 -1
  84. package/packages/web/dist/assets/{ResizableTextarea-Cw6aL4rp.js → ResizableTextarea-DiIOEGjN.js} +1 -1
  85. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +1 -0
  86. package/packages/web/dist/assets/SessionCard-BMGC2HqI.css +1 -0
  87. package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +1 -0
  88. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +36 -0
  89. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +1 -0
  90. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  91. package/packages/web/dist/assets/SessionFormOptions-DYUISplS.js +1 -0
  92. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +1 -0
  93. package/packages/web/dist/assets/{SessionListView-DVhoZHN9.css → SessionListView-fHlQyecX.css} +1 -1
  94. package/packages/web/dist/assets/{SessionLogStream-DIndOyFR.js → SessionLogStream-DpUE6Xsh.js} +1 -1
  95. package/packages/web/dist/assets/{SettingsView-CmJ5JPd5.js → SettingsView-BC055tIA.js} +1 -1
  96. package/packages/web/dist/assets/SlashCommandWizard-DmTyNG9O.js +1 -0
  97. package/packages/web/dist/assets/SlashCommandWizard-Dn7sNaBd.css +1 -0
  98. package/packages/web/dist/assets/SummarySettingsView-BgnRCwlq.js +1 -0
  99. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  100. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +1 -0
  101. package/packages/web/dist/assets/{commandButtons-D74TkPNU.js → commandButtons-D4RPpLiu.js} +1 -1
  102. package/packages/web/dist/assets/index-4rhEeO0B.js +1 -0
  103. package/packages/web/dist/assets/index-9vb2KaAd.js +1 -0
  104. package/packages/web/dist/assets/index-B0CvZXuN.js +7 -0
  105. package/packages/web/dist/assets/index-B6G18FqB.js +82 -0
  106. package/packages/web/dist/assets/{index-DMZZCi2u.js → index-BGwH4Cfn.js} +3 -3
  107. package/packages/web/dist/assets/index-BUhvkAdF.js +1 -0
  108. package/packages/web/dist/assets/index-BcnkUk2o.js +1 -0
  109. package/packages/web/dist/assets/{index-DQMHi05L.js → index-Bn5xdGFM.js} +2 -2
  110. package/packages/web/dist/assets/index-CNwkdB0T.js +1 -0
  111. package/packages/web/dist/assets/index-CfL84oGW.js +1 -0
  112. package/packages/web/dist/assets/index-CkmxO8Mm.js +1 -0
  113. package/packages/web/dist/assets/index-Cpy4-yv3.js +1 -0
  114. package/packages/web/dist/assets/index-CrAQJmoZ.js +1 -0
  115. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-Cs2nxhrT.css} +1 -1
  116. package/packages/web/dist/assets/index-D6Ky9vJe.js +3 -0
  117. package/packages/web/dist/assets/index-DfrE0gAC.js +1 -0
  118. package/packages/web/dist/assets/index-KwEyz0F3.js +1 -0
  119. package/packages/web/dist/assets/index-OfCywayk.js +1 -0
  120. package/packages/web/dist/assets/index-PDesaJc6.js +1 -0
  121. package/packages/web/dist/assets/index-uB6nhSvz.js +1 -0
  122. package/packages/web/dist/assets/{projects-D_C9dE9s.js → projects-BUiOGmmb.js} +1 -1
  123. package/packages/web/dist/assets/providers-Bh1ZiiJi.js +1 -0
  124. package/packages/web/dist/assets/sessions-DH1R-NhV.js +1 -0
  125. package/packages/web/dist/assets/settings-Z4AVVmkJ.js +1 -0
  126. package/packages/web/dist/index.html +2 -2
  127. package/packages/web/dist/assets/ActiveSessionsView-BafIafEu.js +0 -1
  128. package/packages/web/dist/assets/ApiClient-CcqJ-GAv.js +0 -1
  129. package/packages/web/dist/assets/EffortLevelSelector-xE3gidpq.js +0 -1
  130. package/packages/web/dist/assets/MarkdownEditor-HCKnwRye.js +0 -2
  131. package/packages/web/dist/assets/ModelSelector-B0RdlCHT.js +0 -1
  132. package/packages/web/dist/assets/ProjectEditView-BBHOsgBV.js +0 -1
  133. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  134. package/packages/web/dist/assets/ProvidersView-Eg93KbyC.js +0 -1
  135. package/packages/web/dist/assets/ProvidersView-uD8SKWpA.css +0 -1
  136. package/packages/web/dist/assets/QuickResponseSettings-BBHMapcA.js +0 -1
  137. package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +0 -1
  138. package/packages/web/dist/assets/SessionCard-CCapYVjy.js +0 -1
  139. package/packages/web/dist/assets/SessionCard-CcqIjL8q.css +0 -1
  140. package/packages/web/dist/assets/SessionDetailView-BL83oPiI.css +0 -1
  141. package/packages/web/dist/assets/SessionDetailView-CrZvMb3j.js +0 -36
  142. package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +0 -1
  143. package/packages/web/dist/assets/SessionFormOptions-Em7sQCGb.js +0 -1
  144. package/packages/web/dist/assets/SessionListView-3zdDtqhw.js +0 -1
  145. package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +0 -1
  146. package/packages/web/dist/assets/SlashCommandWizard-C_cSgF-P.js +0 -1
  147. package/packages/web/dist/assets/SummarySettingsView-DQM1n3bc.js +0 -1
  148. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  149. package/packages/web/dist/assets/TemplateDetailView-B8clSBPk.js +0 -1
  150. package/packages/web/dist/assets/index-B5ocUoPf.js +0 -1
  151. package/packages/web/dist/assets/index-BELtFs3n.js +0 -1
  152. package/packages/web/dist/assets/index-BGAW2Nqa.js +0 -82
  153. package/packages/web/dist/assets/index-BsDR4w2c.js +0 -1
  154. package/packages/web/dist/assets/index-CVozYqQ-.js +0 -3
  155. package/packages/web/dist/assets/index-CefzeYRE.js +0 -1
  156. package/packages/web/dist/assets/index-CrLh8vw5.js +0 -1
  157. package/packages/web/dist/assets/index-DIvveuSK.js +0 -1
  158. package/packages/web/dist/assets/index-DPt6qBRK.js +0 -1
  159. package/packages/web/dist/assets/index-DYWZ8lD-.js +0 -1
  160. package/packages/web/dist/assets/index-DrlwE0Zo.js +0 -7
  161. package/packages/web/dist/assets/index-DuXChAe-.js +0 -1
  162. package/packages/web/dist/assets/index-Dz7jFUYU.js +0 -1
  163. package/packages/web/dist/assets/index-Gre8tUfC.js +0 -1
  164. package/packages/web/dist/assets/index-_Lv79l46.js +0 -1
  165. package/packages/web/dist/assets/index-f315nDFm.js +0 -1
  166. package/packages/web/dist/assets/index-rjbX81sm.js +0 -1
  167. package/packages/web/dist/assets/providers-BdvbPVdE.js +0 -1
  168. package/packages/web/dist/assets/sessions-Bs5FA6JZ.js +0 -1
  169. package/packages/web/dist/assets/settings-6Rw9xt-G.js +0 -1
@@ -1,5 +1,6 @@
1
1
  import { sessions, messages, attachments, conversations } from '../database.js';
2
2
  import { createClaudeCodeSpawner } from './nodeSpawnHelper.js';
3
+ import { createCodexSpawner } from './codexSpawnHelper.js';
3
4
  import { resolveProviderFromModel, buildSessionEnv } from './sessionProvider.js';
4
5
  import { agentGateway } from '../agents/AgentGateway.js';
5
6
  import { LoggingAgentWrapper } from '../agents/LoggingAgentWrapper.js';
@@ -7,6 +8,7 @@ import { VCRAgentAdapter } from '../agents/vcr/VCRAgentAdapter.js';
7
8
  import {
8
9
  buildSystemPromptConfig,
9
10
  getPermissionModeForSession,
11
+ getSandboxModeForSession,
10
12
  buildPromptWithAttachments,
11
13
  } from './sessionPrompts.js';
12
14
  import {
@@ -20,18 +22,38 @@ import {
20
22
  } from './streamEventHandler.js';
21
23
  import { shouldRescheduleOnError, _checkProactiveReschedule } from './sessionErrors.js';
22
24
  import { schedulerService } from './schedulerService.js';
23
- import { buildConversationContextForModelSwitch } from './conversationContext.js';
25
+ import { buildConversationContextForModelSwitch, buildConversationContextForContinuation } from './conversationContext.js';
24
26
  import { broadcastToSession } from '../websocket.js';
25
27
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
26
28
 
29
+ /**
30
+ * Build the adapter-specific default config object for
31
+ * {@link createAgentForSession}. Callers may pass an explicit `config` to
32
+ * override these defaults.
33
+ * @param {string} agentType
34
+ * @returns {Object}
35
+ */
36
+ function buildAgentConfig(agentType) {
37
+ if (agentType === 'codex') {
38
+ return { spawnCodexProcess: createCodexSpawner() };
39
+ }
40
+ return {};
41
+ }
42
+
27
43
  /**
28
44
  * Create the agent for a session, using gateway + logging + VCR.
29
45
  *
30
- * @param {string} agentType - The agent type (e.g., 'claude-code')
46
+ * If `config` is empty, the adapter-specific default config is applied
47
+ * (e.g. codex receives a fresh `spawnCodexProcess` spawner). Explicit
48
+ * `config` keys win over defaults.
49
+ *
50
+ * @param {string} agentType - The agent type (e.g., 'claude-code', 'codex')
51
+ * @param {Object} [config] - Optional adapter config forwarded to the gateway.
31
52
  * @returns {{ execute: (queryParams: any, meta?: any) => AsyncGenerator }}
32
53
  */
33
- export function createAgentForSession(agentType = 'claude-code') {
34
- const baseAgent = agentGateway.createAgent(agentType);
54
+ export function createAgentForSession(agentType = 'claude-code', config = {}) {
55
+ const mergedConfig = { ...buildAgentConfig(agentType), ...config };
56
+ const baseAgent = agentGateway.createAgent(agentType, mergedConfig);
35
57
 
36
58
  // Wrap with VCR adapter if in VCR mode
37
59
  const agent = process.env.VCR_MODE
@@ -43,22 +65,13 @@ export function createAgentForSession(agentType = 'claude-code') {
43
65
  }
44
66
 
45
67
  /**
46
- * Build query parameters for executing a session via the Claude agent.
47
- * Shared by runSession, continueSession, and continueSessionWithExistingMessage.
48
- * @param {Object} options
49
- * @param {string} options.prompt - The prompt text to send
50
- * @param {string} options.workingDirectory - Session working directory
51
- * @param {AbortController} options.controller - Abort controller for the session
52
- * @param {Object} options.session - Session object from DB
53
- * @param {string} options.sessionId - Session ID
54
- * @param {string|null} options.systemPrompt - Custom system prompt from project settings
55
- * @param {string|null} options.model - Model to use
56
- * @param {Object} options.sessionEnv - Environment variables for the session
57
- * @param {string|null} [options.resumeSessionId] - Claude session ID to resume (null for new session)
58
- * @returns {Object} Query parameters for agent.execute()
68
+ * Build query parameters for the Claude Code adapter.
69
+ * @returns {Object}
59
70
  */
60
- export function buildQueryParams({ prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv, resumeSessionId = null }) {
61
- // Determine model (force Haiku in VCR mode to minimize cost)
71
+ function buildClaudeCodeQueryParams({
72
+ prompt, workingDirectory, controller, session, sessionId, systemPrompt,
73
+ model, sessionEnv, resumeSessionId = null,
74
+ }) {
62
75
  const isVCR = Boolean(process.env.VCR_MODE);
63
76
  const effectiveModel = isVCR ? 'claude-haiku-4-5-20251001' : model;
64
77
 
@@ -69,7 +82,9 @@ export function buildQueryParams({ prompt, workingDirectory, controller, session
69
82
  abortController: controller,
70
83
  includePartialMessages: true,
71
84
  permissionMode: getPermissionModeForSession(session.mode),
72
- settingSources: ['project'],
85
+ // Match normal Claude Code CLI behavior: load user-level settings
86
+ // such as configured MCP servers, then project/local overrides.
87
+ settingSources: ['user', 'project', 'local'],
73
88
  ...(resumeSessionId && { resume: resumeSessionId }),
74
89
  env: sessionEnv,
75
90
  spawnClaudeCodeProcess: createClaudeCodeSpawner(),
@@ -79,6 +94,67 @@ export function buildQueryParams({ prompt, workingDirectory, controller, session
79
94
  };
80
95
  }
81
96
 
97
+ /**
98
+ * Build query parameters for the Codex adapter.
99
+ *
100
+ * Codex in v1 is a simple Chat-Completions-shaped executor — it doesn't need
101
+ * or accept Claude-specific options (permissionMode, settingSources,
102
+ * includePartialMessages, spawnClaudeCodeProcess, resume).
103
+ *
104
+ * Codex does have its own sandboxing model, driven from {@code session.mode}
105
+ * via {@link getSandboxModeForSession}. Codex CLI v0.124.0 also supports
106
+ * resume via `codex resume` / `codex exec resume`, but Circus Chief v1
107
+ * intentionally does NOT pass a resume token — wiring is deferred to a
108
+ * later phase (see canvas plan §Phase 4.5).
109
+ *
110
+ * @returns {Object}
111
+ */
112
+ function buildCodexQueryParams({
113
+ prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv,
114
+ }) {
115
+ const isVCR = Boolean(process.env.VCR_MODE);
116
+ // In VCR mode, force the cheapest commonly-cassetted OpenAI model.
117
+ const effectiveModel = isVCR ? 'gpt-4o-mini' : model;
118
+
119
+ return {
120
+ prompt,
121
+ options: {
122
+ cwd: workingDirectory,
123
+ abortController: controller,
124
+ env: sessionEnv,
125
+ model: effectiveModel,
126
+ effortLevel: session?.effortLevel ?? null,
127
+ systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
128
+ sandboxMode: getSandboxModeForSession(session?.mode),
129
+ },
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Build query parameters for executing a session via the configured agent.
135
+ * Shared by runSession, continueSession, and continueSessionWithExistingMessage.
136
+ *
137
+ * @param {Object} options
138
+ * @param {string} options.prompt - The prompt text to send
139
+ * @param {string} options.workingDirectory - Session working directory
140
+ * @param {AbortController} options.controller - Abort controller for the session
141
+ * @param {Object} options.session - Session object from DB
142
+ * @param {string} options.sessionId - Session ID
143
+ * @param {string|null} options.systemPrompt - Custom system prompt from project settings
144
+ * @param {string|null} options.model - Model to use
145
+ * @param {Object} options.sessionEnv - Environment variables for the session
146
+ * @param {string|null} [options.resumeSessionId] - Session ID to resume (null for new session)
147
+ * @param {string} [options.agentType] - 'claude-code' (default) | 'codex'
148
+ * @returns {Object} Query parameters for agent.execute()
149
+ */
150
+ export function buildQueryParams(options) {
151
+ const { agentType = 'claude-code' } = options || {};
152
+ if (agentType === 'codex') {
153
+ return buildCodexQueryParams(options);
154
+ }
155
+ return buildClaudeCodeQueryParams(options);
156
+ }
157
+
82
158
  /**
83
159
  * Execute the agent stream loop and handle post-turn completion, errors, and cleanup.
84
160
  * This is the shared core of runSession, continueSession, and continueSessionWithExistingMessage.
@@ -146,17 +222,26 @@ export async function _executeSession({
146
222
  }
147
223
 
148
224
  /**
149
- * Build prompt with conversation context when switching models.
225
+ * Build prompt with conversation context for a continuation.
150
226
  * When the model changes, we can't resume the previous session, so we include
151
227
  * conversation history as context so the new model can continue naturally.
152
- * @param {boolean} modelChanged
153
- * @param {string} conversationId
154
- * @param {string} promptWithAttachments
228
+ * When the adapter cannot resume, we include conversation history so the
229
+ * model has context of previous turns.
230
+ * @param {Object} opts
231
+ * @param {boolean} opts.modelChanged
232
+ * @param {Object} opts.agent - Agent instance
233
+ * @param {string} opts.conversationId
234
+ * @param {string} opts.prompt
155
235
  * @returns {Promise<string>}
156
236
  */
157
- async function buildPromptForContinue(modelChanged, conversationId, promptWithAttachments) {
158
- if (!modelChanged) return promptWithAttachments;
159
- return buildConversationContextForModelSwitch(conversationId) + promptWithAttachments;
237
+ async function buildPromptForContinue({ modelChanged, agent, conversationId, prompt }) {
238
+ if (modelChanged) {
239
+ return buildConversationContextForModelSwitch(conversationId) + prompt;
240
+ }
241
+ if (agent.needsConversationContext()) {
242
+ return buildConversationContextForContinuation(conversationId) + prompt;
243
+ }
244
+ return prompt;
160
245
  }
161
246
 
162
247
  /**
@@ -201,13 +286,16 @@ function buildContinueModelAndEnv(session, sessionId, model) {
201
286
  async function buildContinueParams({
202
287
  sessionId, session, model, systemPrompt, effectiveModel, sessionEnv,
203
288
  modelChanged, activeConversation, promptWithAttachments,
204
- workingDirectory, controller, agentType,
289
+ workingDirectory, controller, agentType, agent,
205
290
  }) {
206
- // Only resume if we have a session ID AND model hasn't changed
207
- const canResume = activeConversation.claudeSessionId && !modelChanged;
291
+ // Only resume if we have a session ID AND model hasn't changed AND the
292
+ // agent supports resume.
293
+ const canResume = activeConversation.claudeSessionId && !modelChanged && agent.supportsResume();
208
294
 
209
- // Build prompt with conversation context when model changes
210
- const promptWithContext = await buildPromptForContinue(modelChanged, activeConversation.id, promptWithAttachments);
295
+ // Build prompt with conversation context when model changes or adapter needs it
296
+ const promptWithContext = await buildPromptForContinue({
297
+ modelChanged, agent, conversationId: activeConversation.id, prompt: promptWithAttachments,
298
+ });
211
299
 
212
300
  const queryParams = buildQueryParams({
213
301
  prompt: promptWithContext,
@@ -219,6 +307,7 @@ async function buildContinueParams({
219
307
  model: effectiveModel,
220
308
  sessionEnv,
221
309
  resumeSessionId: canResume ? activeConversation.claudeSessionId : null,
310
+ agentType,
222
311
  });
223
312
 
224
313
  // Logging metadata for agent call tracking
@@ -246,6 +335,10 @@ async function setupConversationAndMessage(sessionId, content, fileAttachments)
246
335
  activeConversationIds.set(sessionId, activeConversation.id);
247
336
 
248
337
  const message = messages.create(sessionId, 'user', content, { toolUse: null, conversationId: activeConversation.id });
338
+
339
+ // Touch the session to update its updated_at timestamp so it sorts to the top
340
+ sessions.touch(sessionId);
341
+
249
342
  broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_MESSAGE, {
250
343
  message,
251
344
  conversationId: activeConversation.id,
@@ -307,7 +400,7 @@ export async function continueSessionCore(sessionId, content, workingDirectory,
307
400
  sessionId, session, model, systemPrompt,
308
401
  effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
309
402
  modelChanged: modelEnv.modelChanged, activeConversation, promptWithAttachments,
310
- workingDirectory, controller, agentType,
403
+ workingDirectory, controller, agentType, agent,
311
404
  });
312
405
 
313
406
  await _executeSession({
@@ -384,6 +477,7 @@ export async function runSessionCore(sessionId, prompt, workingDirectory, config
384
477
  systemPrompt,
385
478
  model: effectiveModel,
386
479
  sessionEnv,
480
+ agentType,
387
481
  });
388
482
 
389
483
  // Log query params for debugging third-party provider issues
@@ -3,7 +3,7 @@ import { broadcastToSession, broadcastToProject } from '../websocket.js';
3
3
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
4
4
  import * as summaryService from './summaryService.js';
5
5
  import { checkAndTriggerNextTemplate } from './templateTriggerService.js';
6
- import { resolveProviderFromModel, buildSessionEnv } from './sessionProvider.js';
6
+ import { resolveProviderFromModel, buildSessionEnv, resolveAgentTypeFromModel } from './sessionProvider.js';
7
7
  import {
8
8
  shouldRescheduleOnError,
9
9
  _checkProactiveReschedule,
@@ -18,7 +18,7 @@ import {
18
18
  buildPromptWithAttachments,
19
19
  getApiBaseUrl,
20
20
  } from './sessionPrompts.js';
21
- import { buildConversationContextForModelSwitch, buildConversationContextForBranch } from './conversationContext.js';
21
+ import { buildConversationContextForModelSwitch, buildConversationContextForBranch, buildConversationContextForContinuation } from './conversationContext.js';
22
22
  import {
23
23
  activeSessions,
24
24
  activeConversationIds,
@@ -220,6 +220,13 @@ function validateAndFetchContinueContext(sessionId, conversationId) {
220
220
  /**
221
221
  * Resolve the effective model, provider, and session env from a model override.
222
222
  * Detects model changes and updates the session record when needed.
223
+ *
224
+ * Defense in depth: when a new model arrives and the session has no assistant
225
+ * messages yet (i.e. it is still effectively a draft), re-derive agent_type
226
+ * and persist it together with model. Once the session has produced at least
227
+ * one assistant message we MUST NOT mutate agent_type — that would corrupt
228
+ * resume/context state across kinds.
229
+ *
223
230
  * @param {Object} session - Current session object
224
231
  * @param {string} sessionId - Session ID
225
232
  * @param {string|null} model - Requested model (null to keep current)
@@ -233,13 +240,35 @@ function buildModelAndProvider(session, sessionId, model) {
233
240
 
234
241
  let updatedSession = session;
235
242
  if (model) {
236
- sessions.update(sessionId, { model });
243
+ const update = { model };
244
+
245
+ // Defense in depth: if this is still a draft (no assistant messages),
246
+ // also re-derive agent_type so it stays in sync with the chosen model.
247
+ // After the first assistant turn this is locked.
248
+ if (sessionHasNoAssistantMessages(sessionId)) {
249
+ const derivedAgentType = resolveAgentTypeFromModel(model);
250
+ if (derivedAgentType && derivedAgentType !== session.agentType) {
251
+ update.agentType = derivedAgentType;
252
+ }
253
+ }
254
+
255
+ sessions.update(sessionId, update);
237
256
  updatedSession = sessions.getById(sessionId);
238
257
  }
239
258
 
240
259
  return { effectiveModel, sessionEnv, modelChanged, session: updatedSession };
241
260
  }
242
261
 
262
+ /**
263
+ * Check whether a session has no assistant messages (i.e. is still a draft).
264
+ * @param {string} sessionId
265
+ * @returns {boolean}
266
+ */
267
+ function sessionHasNoAssistantMessages(sessionId) {
268
+ const allMessages = messages.getBySessionId(sessionId);
269
+ return !allMessages.some(m => m.role === 'assistant');
270
+ }
271
+
243
272
  /**
244
273
  * Build query params for continueSessionWithExistingMessage.
245
274
  * Handles context building (model switch / branch) and resume detection.
@@ -248,18 +277,26 @@ function buildModelAndProvider(session, sessionId, model) {
248
277
  function buildExistingMessageQueryParams({
249
278
  sessionId, conversationId, session, model, systemPrompt,
250
279
  effectiveModel, sessionEnv, modelChanged, conversation,
251
- lastUserMessage, workingDirectory, controller, agentType,
280
+ lastUserMessage, workingDirectory, controller, agentType, agent,
252
281
  }) {
253
282
  // Determine context needs and build context
254
283
  const { needsContext, contextType } = determineContextNeed(conversation, modelChanged);
255
284
  if (needsContext) {
256
285
  console.log(`[SESSION] ${contextType === 'modelSwitch' ? 'Model changed' : 'Branched conversation'} - including context`);
257
286
  }
258
- const conversationContext = buildContextForType(conversationId, contextType);
287
+ let conversationContext = buildContextForType(conversationId, contextType);
288
+
289
+ // Fallback: if no specific context was built but the adapter needs
290
+ // conversation context (i.e. it can't resume), inject continuation history.
291
+ if (!conversationContext && agent.needsConversationContext()) {
292
+ conversationContext = buildConversationContextForContinuation(conversationId);
293
+ }
294
+
259
295
  const promptWithContext = conversationContext + lastUserMessage.content;
260
296
 
261
- // Only resume if we have a session ID AND model hasn't changed
262
- const canResume = conversation.claudeSessionId && !modelChanged;
297
+ // Only resume if we have a session ID AND model hasn't changed AND the
298
+ // agent supports resume.
299
+ const canResume = conversation.claudeSessionId && !modelChanged && agent.supportsResume();
263
300
 
264
301
  const queryParams = buildQueryParams({
265
302
  prompt: promptWithContext,
@@ -319,7 +356,7 @@ export async function continueSessionWithExistingMessage(sessionId, conversation
319
356
  sessionId, conversationId, session, model, systemPrompt,
320
357
  effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
321
358
  modelChanged: modelEnv.modelChanged, conversation,
322
- lastUserMessage, workingDirectory, controller, agentType,
359
+ lastUserMessage, workingDirectory, controller, agentType, agent,
323
360
  });
324
361
 
325
362
  await _executeSession({
@@ -1,5 +1,6 @@
1
1
  import { sessions, attachments, projects, kanbanBoards, kanbanLanes } from '../database.js';
2
2
  import { DEFAULT_SERVER_PORT, DEFAULT_SYSTEM_PROMPT } from '../../../shared/src/index.js';
3
+ import { buildCommandButtonApiInstructions } from './commandButtonPrompts.js';
3
4
 
4
5
  /**
5
6
  * Get the base API URL for canvas and session operations.
@@ -108,6 +109,31 @@ export function getPermissionModeForSession(mode) {
108
109
  }
109
110
  }
110
111
 
112
+ /**
113
+ * Map session mode to the Codex CLI --sandbox flag value.
114
+ *
115
+ * plan → read-only (parity with Claude plan mode's read-mostly posture)
116
+ * standard → workspace-write (default; Codex can edit files in cwd)
117
+ * yolo → danger-full-access (parallels Claude's bypassPermissions)
118
+ *
119
+ * Note: `--full-auto` is intentionally NOT used — it is a shorthand that also
120
+ * overrides approval policies, which would conflate two orthogonal concerns.
121
+ *
122
+ * @param {string} mode - Session mode ('plan', 'standard', 'yolo')
123
+ * @returns {string} Codex sandbox mode
124
+ */
125
+ export function getSandboxModeForSession(mode) {
126
+ switch (mode) {
127
+ case 'plan':
128
+ return 'read-only';
129
+ case 'yolo':
130
+ return 'danger-full-access';
131
+ case 'standard':
132
+ default:
133
+ return 'workspace-write';
134
+ }
135
+ }
136
+
111
137
  /** Plan mode system prompt instructions */
112
138
  export const PLAN_MODE_PROMPT = `## Plan Mode Active
113
139
 
@@ -397,10 +423,12 @@ This session is part of a multi-session workflow:
397
423
  * @returns {string} System prompt string
398
424
  */
399
425
  export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt, mode) {
426
+ const apiUrl = getApiBaseUrl();
400
427
  const session = sessions.getById(sessionId);
401
428
  const canvasWriteInstructions = buildCanvasWriteSystemPrompt(session); // Pass session object
402
429
  const canvasReadInstructions = buildCanvasReadSystemPrompt(session); // Pass session object
403
430
  const sessionApiInstructions = buildSessionApiInstructions(sessionId, projectId);
431
+ const commandButtonApiInstructions = buildCommandButtonApiInstructions(apiUrl, sessionId, projectId);
404
432
  const kanbanApiInstructions = buildKanbanApiInstructions(sessionId, projectId);
405
433
  const attachmentsContext = getSessionAttachmentsContext(sessionId);
406
434
  const worktreeContext = buildWorktreeContext(session);
@@ -420,6 +448,7 @@ export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt
420
448
  canvasWriteInstructions,
421
449
  canvasReadInstructions,
422
450
  sessionApiInstructions,
451
+ commandButtonApiInstructions,
423
452
  kanbanApiInstructions
424
453
  ].filter(Boolean);
425
454
 
@@ -12,7 +12,34 @@ export function resolveProviderFromModel(modelId) {
12
12
  }
13
13
 
14
14
  /**
15
- * Build environment variables from provider configuration
15
+ * Resolve the agent type (claude-code vs codex) for a given model ID.
16
+ * Uses the owning provider's kind:
17
+ * - anthropic → claude-code
18
+ * - openai → codex
19
+ * Falls back to 'claude-code' for null / unknown / tier-name inputs.
20
+ * @param {string|null} modelId
21
+ * @returns {string} 'claude-code' | 'codex'
22
+ */
23
+ export function resolveAgentTypeFromModel(modelId) {
24
+ if (!modelId) return 'claude-code';
25
+ const provider = modelProviders.getProviderByModelId(modelId);
26
+ if (!provider) return 'claude-code';
27
+ if (typeof modelProviders.getAgentTypeForProvider === 'function') {
28
+ const agentType = modelProviders.getAgentTypeForProvider(provider.id);
29
+ return agentType || 'claude-code';
30
+ }
31
+ // Fallback for test doubles that don't implement getAgentTypeForProvider:
32
+ // derive from kind directly.
33
+ if (provider.kind === 'openai') return 'codex';
34
+ return 'claude-code';
35
+ }
36
+
37
+ /**
38
+ * Build environment variables from provider configuration.
39
+ * Branches on provider.kind so Anthropic-kind and OpenAI-kind providers
40
+ * emit only their own wire-protocol env vars (no cross-kind leaks).
41
+ * Providers without a `kind` field default to Anthropic behavior for
42
+ * backward compatibility.
16
43
  * @param {Object|null} provider - Provider object
17
44
  * @returns {Object} Environment variables to add to session env
18
45
  */
@@ -22,41 +49,68 @@ export function buildProviderEnv(provider) {
22
49
  return {}; // Use SDK defaults
23
50
  }
24
51
 
25
- const env = {};
52
+ const kind = provider.kind || 'anthropic';
53
+ const env = kind === 'openai'
54
+ ? buildOpenAIProviderEnv(provider)
55
+ : buildAnthropicProviderEnv(provider);
26
56
 
27
- if (provider.baseUrl) {
28
- env.ANTHROPIC_BASE_URL = provider.baseUrl;
57
+ if (provider.apiTimeoutMs) {
58
+ env.API_TIMEOUT_MS = String(provider.apiTimeoutMs);
29
59
  }
30
- if (provider.authToken) {
31
- // Set BOTH ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN
32
- // The SDK prioritizes ANTHROPIC_API_KEY, so we must set it to override
33
- // any user's existing ANTHROPIC_API_KEY in their environment
34
- env.ANTHROPIC_API_KEY = provider.authToken;
35
- env.ANTHROPIC_AUTH_TOKEN = provider.authToken;
60
+
61
+ // Parse additional env vars (applied last so users can override anything above)
62
+ if (provider.additionalEnvVars) {
63
+ Object.assign(env, provider.additionalEnvVars);
36
64
  }
37
65
 
38
- // Derive default model env vars from provider_models by tier (single source of truth)
39
- if (Array.isArray(provider.models)) {
40
- const opusModel = provider.models.find((m) => m.tier === 'opus');
41
- if (opusModel) env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel.modelId;
66
+ logProviderEnv(provider, kind, env);
67
+
68
+ return env;
69
+ }
42
70
 
43
- const sonnetModel = provider.models.find((m) => m.tier === 'sonnet');
44
- if (sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel.modelId;
71
+ function buildOpenAIProviderEnv(provider) {
72
+ const env = {};
73
+ if (provider.baseUrl) env.OPENAI_BASE_URL = provider.baseUrl;
74
+ if (provider.authToken) env.OPENAI_API_KEY = provider.authToken;
75
+ return env;
76
+ }
45
77
 
46
- const haikuModel = provider.models.find((m) => m.tier === 'haiku');
47
- if (haikuModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel.modelId;
78
+ function buildAnthropicProviderEnv(provider) {
79
+ const env = {};
80
+ if (provider.baseUrl) env.ANTHROPIC_BASE_URL = provider.baseUrl;
81
+ if (provider.authToken) {
82
+ env.ANTHROPIC_API_KEY = provider.authToken;
83
+ env.ANTHROPIC_AUTH_TOKEN = provider.authToken;
48
84
  }
85
+ addAnthropicModelEnv(env, provider.models);
86
+ return env;
87
+ }
49
88
 
50
- if (provider.apiTimeoutMs) {
51
- env.API_TIMEOUT_MS = String(provider.apiTimeoutMs);
89
+ function addAnthropicModelEnv(env, models) {
90
+ if (!Array.isArray(models)) return;
91
+ const target = env;
92
+ const tiers = {
93
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
94
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
95
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
96
+ };
97
+ for (const [tier, envKey] of Object.entries(tiers)) {
98
+ const model = models.find((entry) => entry.tier === tier);
99
+ if (model) target[envKey] = model.modelId;
52
100
  }
101
+ }
53
102
 
54
- // Parse additional env vars
55
- if (provider.additionalEnvVars) {
56
- Object.assign(env, provider.additionalEnvVars);
103
+ function logProviderEnv(provider, kind, env) {
104
+ if (kind === 'openai') {
105
+ console.log(`[SessionManager] buildProviderEnv: Provider "${provider.name}" (openai) env vars:`, {
106
+ OPENAI_BASE_URL: env.OPENAI_BASE_URL,
107
+ OPENAI_API_KEY: env.OPENAI_API_KEY ? '[SET]' : '[NOT SET]',
108
+ API_TIMEOUT_MS: env.API_TIMEOUT_MS,
109
+ });
110
+ return;
57
111
  }
58
112
 
59
- console.log(`[SessionManager] buildProviderEnv: Provider "${provider.name}" env vars:`, {
113
+ console.log(`[SessionManager] buildProviderEnv: Provider "${provider.name}" (anthropic) env vars:`, {
60
114
  ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL,
61
115
  ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY ? '[SET]' : '[NOT SET]',
62
116
  ANTHROPIC_AUTH_TOKEN: env.ANTHROPIC_AUTH_TOKEN ? '[SET]' : '[NOT SET]',
@@ -64,15 +118,24 @@ export function buildProviderEnv(provider) {
64
118
  ANTHROPIC_DEFAULT_OPUS_MODEL: env.ANTHROPIC_DEFAULT_OPUS_MODEL,
65
119
  ANTHROPIC_DEFAULT_HAIKU_MODEL: env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
66
120
  });
67
-
68
- return env;
69
121
  }
70
122
 
71
123
  /**
72
- * Build environment variables for Claude SDK based on provider and session settings.
124
+ * Build environment variables for the agent runtime based on provider and session settings.
73
125
  * Always returns a robust env with Node in PATH to prevent ENOENT errors.
74
- * @param {Object|null} provider - Provider object or null for Anthropic defaults
126
+ *
127
+ * Kind-aware behavior:
128
+ * - provider.kind === 'anthropic' (or legacy/unspecified): keeps today's behavior
129
+ * (MAX_THINKING_TOKENS + CLAUDE_CODE_EFFORT_LEVEL are applied as before).
130
+ * - provider.kind === 'openai': Claude-only envs (MAX_THINKING_TOKENS,
131
+ * CLAUDE_CODE_EFFORT_LEVEL) are NOT set, and any ANTHROPIC_* vars from
132
+ * process.env are stripped so Claude env doesn't leak into Codex sessions.
133
+ * - provider === null: strip BOTH kinds' auth/base-url vars so host env
134
+ * doesn't bleed into the SDK defaults.
135
+ *
136
+ * @param {Object|null} provider - Provider object or null for agent defaults
75
137
  * @param {boolean} thinkingEnabled - Whether thinking mode is enabled
138
+ * @param {string|null} effortLevel - Optional effort level
76
139
  * @returns {Object}
77
140
  */
78
141
  export function buildSessionEnv(provider, thinkingEnabled = false, effortLevel = null) {
@@ -82,26 +145,82 @@ export function buildSessionEnv(provider, thinkingEnabled = false, effortLevel =
82
145
  // Combine all env vars
83
146
  const sessionEnv = {
84
147
  ...baseEnv,
85
- ...providerEnv, // Add provider env vars
148
+ ...providerEnv, // Add provider env vars (wins over host env for its own keys)
86
149
  };
87
150
 
88
- // When no custom provider is configured, explicitly exclude ANTHROPIC_* variables
89
- // from the environment to ensure SDK uses its defaults (not user's env vars)
151
+ const kind = provider?.kind || (provider ? 'anthropic' : null);
152
+
90
153
  if (!provider) {
91
- delete sessionEnv.ANTHROPIC_API_KEY;
92
- delete sessionEnv.ANTHROPIC_AUTH_TOKEN;
93
- delete sessionEnv.ANTHROPIC_BASE_URL;
154
+ stripProviderRuntimeEnv(sessionEnv);
155
+ } else if (kind === 'openai') {
156
+ applyOpenAISessionEnv(sessionEnv, providerEnv);
157
+ } else {
158
+ stripOpenAIHostEnv(sessionEnv);
94
159
  }
95
160
 
96
- // Add thinking tokens if enabled (but suppress in VCR mode to minimize cost)
97
- if (thinkingEnabled && !process.env.VCR_MODE) {
98
- sessionEnv.MAX_THINKING_TOKENS = '10240';
99
- }
161
+ // Claude-only session env vars. Only set for Anthropic-kind providers
162
+ // (or when no provider is configured → Claude-default flow).
163
+ const isClaudeFlow = !provider || kind === 'anthropic';
164
+
165
+ if (isClaudeFlow) {
166
+ // Add thinking tokens if enabled (but suppress in VCR mode to minimize cost)
167
+ if (thinkingEnabled && !process.env.VCR_MODE) {
168
+ sessionEnv.MAX_THINKING_TOKENS = '10240';
169
+ }
100
170
 
101
- // Set effort level if provided
102
- if (effortLevel) {
103
- sessionEnv.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
171
+ // Set effort level if provided
172
+ if (effortLevel) {
173
+ sessionEnv.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
174
+ }
104
175
  }
105
176
 
106
177
  return sessionEnv;
107
178
  }
179
+
180
+ function stripProviderRuntimeEnv(env) {
181
+ const target = env;
182
+ delete target.ANTHROPIC_API_KEY;
183
+ delete target.ANTHROPIC_AUTH_TOKEN;
184
+ delete target.ANTHROPIC_BASE_URL;
185
+ delete target.OPENAI_API_KEY;
186
+ delete target.OPENAI_BASE_URL;
187
+ }
188
+
189
+ function applyOpenAISessionEnv(sessionEnv, providerEnv) {
190
+ stripAnthropicHostEnv(sessionEnv);
191
+ if (!providerEnv.OPENAI_API_KEY) {
192
+ replaceWithCodexCliEnv(sessionEnv, providerEnv);
193
+ return;
194
+ }
195
+ stripOpenAIBaseUrlUnlessProvided(sessionEnv, providerEnv);
196
+ }
197
+
198
+ function stripAnthropicHostEnv(env) {
199
+ const target = env;
200
+ delete target.ANTHROPIC_API_KEY;
201
+ delete target.ANTHROPIC_AUTH_TOKEN;
202
+ delete target.ANTHROPIC_BASE_URL;
203
+ }
204
+
205
+ function replaceWithCodexCliEnv(sessionEnv, providerEnv) {
206
+ const target = sessionEnv;
207
+ delete target.OPENAI_API_KEY;
208
+ delete target.OPENAI_BASE_URL;
209
+ delete target.OPENAI_API_BASE;
210
+ delete target.OPENAI_ORG_ID;
211
+ delete target.OPENAI_PROJECT;
212
+ Object.assign(target, providerEnv);
213
+ }
214
+
215
+ function stripOpenAIBaseUrlUnlessProvided(sessionEnv, providerEnv) {
216
+ if (providerEnv.OPENAI_BASE_URL || providerEnv.OPENAI_API_BASE) return;
217
+ const target = sessionEnv;
218
+ delete target.OPENAI_BASE_URL;
219
+ delete target.OPENAI_API_BASE;
220
+ }
221
+
222
+ function stripOpenAIHostEnv(env) {
223
+ const target = env;
224
+ delete target.OPENAI_API_KEY;
225
+ delete target.OPENAI_BASE_URL;
226
+ }