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
@@ -32,6 +32,7 @@ const SESSIONS_BASE_COLUMNS = `
32
32
  parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
33
33
  input_tokens INTEGER DEFAULT 0,
34
34
  output_tokens INTEGER DEFAULT 0,
35
+ thinking_tokens INTEGER DEFAULT 0,
35
36
  cache_read_input_tokens INTEGER DEFAULT 0,
36
37
  cache_creation_input_tokens INTEGER DEFAULT 0,
37
38
  web_search_requests INTEGER DEFAULT 0,
@@ -59,7 +60,7 @@ const SESSIONS_ALL_COLUMNS = [
59
60
  'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
60
61
  'git_branch', 'git_worktree', 'pr_url', 'error', 'effort_level',
61
62
  'cost_usd', 'claude_session_id', 'model', 'next_template_id',
62
- 'parent_session_id', 'input_tokens', 'output_tokens',
63
+ 'parent_session_id', 'input_tokens', 'output_tokens', 'thinking_tokens',
63
64
  'cache_read_input_tokens', 'cache_creation_input_tokens',
64
65
  'web_search_requests', 'context_window', 'archived', 'starred',
65
66
  'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
@@ -211,6 +212,10 @@ export const sessionsMigrations = [
211
212
  name: 'sessions-add-output_tokens',
212
213
  up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
213
214
  },
215
+ {
216
+ name: 'sessions-add-thinking_tokens',
217
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
218
+ },
214
219
  {
215
220
  name: 'sessions-add-cache_read_input_tokens',
216
221
  up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
@@ -24,6 +24,7 @@ export function mapTokenUsage(row) {
24
24
  return {
25
25
  inputTokens: row.input_tokens || 0,
26
26
  outputTokens: row.output_tokens || 0,
27
+ thinkingTokens: row.thinking_tokens || 0,
27
28
  cacheReadInputTokens: row.cache_read_input_tokens || 0,
28
29
  cacheCreationInputTokens: row.cache_creation_input_tokens || 0,
29
30
  webSearchRequests: row.web_search_requests || 0,
@@ -58,7 +59,12 @@ const CONFIG_DEFAULTS = {
58
59
  parentSessionId: null,
59
60
  status: 'starting',
60
61
  model: null,
62
+ providerId: null,
61
63
  effortLevel: null,
64
+ // Agent runtime for the session: 'claude-code' (default) or 'codex'.
65
+ // Defaults to null so SessionRepository.create() can resolve it from the model.
66
+ // Explicit values from callers are preserved as-is.
67
+ agentType: null,
62
68
  };
63
69
 
64
70
  function buildConfig(src) {
@@ -96,6 +102,7 @@ export const DIRECT_FIELD_MAP = {
96
102
  costUsd: 'cost_usd',
97
103
  claudeSessionId: 'claude_session_id',
98
104
  model: 'model',
105
+ providerId: 'provider_id',
99
106
  nextTemplateId: 'next_template_id',
100
107
  parentSessionId: 'parent_session_id',
101
108
  scheduledAt: 'scheduled_at',
@@ -109,6 +116,7 @@ export const DIRECT_FIELD_MAP = {
109
116
  effortLevel: 'effort_level',
110
117
  targetLaneId: 'target_lane_id',
111
118
  laneTriggerDepth: 'lane_trigger_depth',
119
+ agentType: 'agent_type',
112
120
  };
113
121
 
114
122
  /** camelCase -> snake_case column mapping for boolean fields (converted to 1/0) */
@@ -48,6 +48,13 @@ CREATE TABLE IF NOT EXISTS sessions (
48
48
  claude_session_id TEXT,
49
49
  next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
50
50
  parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
51
+ input_tokens INTEGER DEFAULT 0,
52
+ output_tokens INTEGER DEFAULT 0,
53
+ thinking_tokens INTEGER DEFAULT 0,
54
+ cache_read_input_tokens INTEGER DEFAULT 0,
55
+ cache_creation_input_tokens INTEGER DEFAULT 0,
56
+ web_search_requests INTEGER DEFAULT 0,
57
+ context_window INTEGER DEFAULT 200000,
51
58
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
52
59
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
53
60
  );
@@ -64,6 +71,7 @@ CREATE TABLE IF NOT EXISTS conversations (
64
71
  -- Token usage fields (per-conversation tracking)
65
72
  input_tokens INTEGER DEFAULT 0,
66
73
  output_tokens INTEGER DEFAULT 0,
74
+ thinking_tokens INTEGER DEFAULT 0,
67
75
  cache_read_input_tokens INTEGER DEFAULT 0,
68
76
  cache_creation_input_tokens INTEGER DEFAULT 0,
69
77
  web_search_requests INTEGER DEFAULT 0,
@@ -233,6 +241,7 @@ CREATE TABLE IF NOT EXISTS providers (
233
241
  api_timeout_ms INTEGER,
234
242
  additional_env_vars TEXT,
235
243
  is_built_in INTEGER NOT NULL DEFAULT 0,
244
+ kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
236
245
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
237
246
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
238
247
  );
@@ -20,7 +20,7 @@ export class AgentCallLogger {
20
20
  const callId = nanoid();
21
21
 
22
22
  // Build metadata object - only include keys with defined values
23
- const metadata = {};
23
+ const metadata = { ...(meta.metadata || {}) };
24
24
  if (meta.effortLevel !== undefined && meta.effortLevel !== null) {
25
25
  metadata.effortLevel = meta.effortLevel;
26
26
  }
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'child_process';
2
+ import { createRobustEnv } from './nodeSpawnHelper.js';
3
+
4
+ /**
5
+ * Create a custom spawn function for the Codex CLI.
6
+ *
7
+ * Mirrors {@link createClaudeCodeSpawner} but keeps stderr piped (the Codex
8
+ * adapter surfaces stderr bytes as error events rather than silently ignoring
9
+ * them).
10
+ *
11
+ * As with the Claude helper:
12
+ * - The command 'node' is replaced with {@link process.execPath} so child
13
+ * processes use the same Node binary that's running the app (important
14
+ * for nvm/fnm/volta users).
15
+ * - `createRobustEnv` guarantees the Node bin directory is on PATH.
16
+ *
17
+ * @returns {Function} Spawn function of shape (options) => childProcess
18
+ */
19
+ export function createCodexSpawner() {
20
+ return (options) => {
21
+ const { command, args, cwd, env, signal } = options;
22
+
23
+ // Replace 'node' with the absolute path to the current Node executable
24
+ const actualCommand = command === 'node' ? process.execPath : command;
25
+
26
+ // Ensure PATH includes the directory containing Node
27
+ const robustEnv = createRobustEnv(env);
28
+
29
+ return spawn(actualCommand, args, {
30
+ cwd,
31
+ stdio: ['pipe', 'pipe', 'pipe'],
32
+ signal,
33
+ env: robustEnv,
34
+ windowsHide: true,
35
+ });
36
+ };
37
+ }
@@ -0,0 +1,48 @@
1
+ import { commandButtons } from '../database.js';
2
+
3
+ /**
4
+ * Build Command Button API instructions for system prompt if the project has command buttons.
5
+ * @param {string} apiUrl - Base API URL
6
+ * @param {string} sessionId - Current session ID
7
+ * @param {string} projectId - Current project ID
8
+ * @returns {string} Command button instructions or empty string if no buttons configured
9
+ */
10
+ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
11
+ const buttons = commandButtons.getByProjectId(projectId);
12
+ if (!buttons || buttons.length === 0) {
13
+ return '';
14
+ }
15
+
16
+ return `## Command Buttons API
17
+
18
+ This project has command buttons configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
19
+
20
+ ### List Available Buttons
21
+ \`\`\`bash
22
+ curl ${apiUrl}/api/projects/${projectId}/command-buttons
23
+ \`\`\`
24
+
25
+ ### Run a Button
26
+ \`\`\`bash
27
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
28
+ \`\`\`
29
+
30
+ Response: { runId, buttonId, status: "running", output: "" }
31
+
32
+ ### Check Run Status & Output
33
+ \`\`\`bash
34
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
35
+ \`\`\`
36
+
37
+ Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
38
+
39
+ ### List All Runs for This Session
40
+ \`\`\`bash
41
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
42
+ \`\`\`
43
+
44
+ ### Kill a Running Command
45
+ \`\`\`bash
46
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
47
+ \`\`\``;
48
+ }
@@ -70,3 +70,30 @@ ${transcript}
70
70
 
71
71
  `;
72
72
  }
73
+
74
+ /**
75
+ * Build context for a continuation where the adapter cannot resume.
76
+ * This is the generic case for any non-resumable adapter (Codex, future adapters).
77
+ * @param {string} conversationId
78
+ * @returns {string}
79
+ */
80
+ export function buildConversationContextForContinuation(conversationId) {
81
+ const conversationMessages = messages.getByConversationId(conversationId);
82
+
83
+ // Don't include the last user message (that's the current prompt)
84
+ const previousMessages = conversationMessages.slice(0, -1);
85
+
86
+ if (previousMessages.length === 0) {
87
+ return '';
88
+ }
89
+
90
+ const transcript = formatConversationHistory(previousMessages);
91
+
92
+ return `<conversation_history>
93
+ The following is the conversation history from this session so far. Continue naturally from where the conversation left off.
94
+
95
+ ${transcript}
96
+ </conversation_history>
97
+
98
+ `;
99
+ }
@@ -2,6 +2,7 @@ import { sessions, messages, projects, conversations, attachments } from '../dat
2
2
  import { broadcastToSession, broadcastToProject } from '../websocket.js';
3
3
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
4
4
  import * as slashCommandService from './slashCommandService.js';
5
+ import { resolveAgentTypeFromModel } from './sessionProvider.js';
5
6
 
6
7
  /**
7
8
  * Validates that a session is a draft (waiting status with no assistant messages).
@@ -111,6 +112,7 @@ function getOrCreateInitialMessage(session, options) {
111
112
  * @param {object} options
112
113
  * @param {string} [options.prompt] - Optional new prompt to use/override
113
114
  * @param {string} [options.model] - Optional model override
115
+ * @param {string|null} [options.providerId] - Optional provider override
114
116
  * @returns {Promise<object>} The updated session
115
117
  */
116
118
  export async function startDraft(session, options = {}) {
@@ -125,6 +127,11 @@ export async function startDraft(session, options = {}) {
125
127
  // Model to use for this session (optional - SDK will use default if not provided)
126
128
  const model = options.model || session.pendingModel || session.model || null;
127
129
 
130
+ // Resolve the agent type from the selected model before launching.
131
+ // Draft sessions have no assistant messages yet, so the session is still
132
+ // choosing its initial runtime – the selected model determines agentType.
133
+ const agentType = model ? resolveAgentTypeFromModel(model) : session.agentType;
134
+
128
135
  // Get or create the initial user message
129
136
  const initialMessage = getOrCreateInitialMessage(session, options);
130
137
  const finalPrompt = initialMessage.content;
@@ -132,8 +139,14 @@ export async function startDraft(session, options = {}) {
132
139
  // Get session attachments for context
133
140
  const sessionAttachments = attachments.getBySessionId(session.id);
134
141
 
135
- // Update session status to starting and clear pendingModel (mirrors pendingPrompt cleanup above)
136
- sessions.update(session.id, { status: 'starting', pendingModel: null });
142
+ // Update session status to starting, clear pendingModel, and persist the
143
+ // resolved model + agentType BEFORE runSession() reads them from storage.
144
+ sessions.update(session.id, {
145
+ status: 'starting',
146
+ pendingModel: null,
147
+ ...(model ? { model, agentType } : {}),
148
+ ...(options.providerId !== undefined ? { providerId: options.providerId } : {}),
149
+ });
137
150
 
138
151
  // Resolve skill/command invocations so skill body goes into system prompt
139
152
  const resolved = await slashCommandService.resolvePromptSkillOrCommand(
@@ -9,6 +9,7 @@ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
9
9
  import { renderTemplatePrompt, getRootSession } from './templateTriggerService.js';
10
10
  import { setupGitForSession } from './gitSessionSetup.js';
11
11
  import { runSession } from './sessionManager.js';
12
+ import { resolveAgentTypeFromModel } from './sessionProvider.js';
12
13
 
13
14
  // Maximum depth for recursive lane-entry template triggers
14
15
  export const MAX_LANE_TRIGGER_DEPTH = 5;
@@ -147,6 +148,7 @@ async function buildChildSessionFromTemplate(template, session, lane, depth) {
147
148
  gitBranch: settings.gitBranch,
148
149
  status: 'starting',
149
150
  model: settings.model,
151
+ agentType: resolveAgentTypeFromModel(settings.model),
150
152
  });
151
153
 
152
154
  // Configure session
@@ -240,6 +242,7 @@ async function buildChildSessionFromPrompt(lane, session, depth) {
240
242
  const newSession = sessions.create(session.projectId, `Lane prompt (lane: ${lane.name})`, renderedPrompt, {
241
243
  ...settings,
242
244
  status: 'starting',
245
+ agentType: resolveAgentTypeFromModel(settings.model),
243
246
  });
244
247
 
245
248
  // Configure session
@@ -1,35 +1,53 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
+ import OpenAI from 'openai';
2
3
 
3
4
  /**
4
- * Test a provider configuration by making a minimal API call
5
- * @param {Object} config - Provider configuration to test
5
+ * Test a provider configuration by making a minimal API call.
6
+ * Branches on `kind`:
7
+ * - 'anthropic' → send a tiny `messages.create` via `@anthropic-ai/sdk`.
8
+ * - 'openai' → prefer `models.list()` via `openai`; fall back to a
9
+ * `chat.completions.create({ max_tokens: 1 })` if
10
+ * `models.list` is not supported (chat-only endpoints).
11
+ *
12
+ * Both branches return the same response shape:
13
+ * - Success: { success: true, message, details: { model, usage? } }
14
+ * - Failure: { success: false, message, details: { code, type } }
15
+ * This function never throws. Errors are mapped to the failure shape above.
16
+ *
17
+ * @param {Object} config
18
+ * @param {'anthropic'|'openai'} [config.kind='anthropic'] - Provider kind
6
19
  * @param {string} [config.baseUrl] - Base URL for the provider
7
20
  * @param {string} [config.authToken] - Auth token for the provider
8
- * @param {string} [config.defaultSonnetModel] - Default Sonnet model ID
21
+ * @param {string} [config.defaultSonnetModel] - For anthropic: model to test against
9
22
  * @param {number} [config.apiTimeoutMs] - API timeout in milliseconds
10
23
  * @returns {Promise<{success: boolean, message: string, details?: Object}>}
11
24
  */
12
25
  export async function testProviderConnection(config) {
26
+ const { kind = 'anthropic' } = config || {};
27
+ if (kind === 'openai') {
28
+ return testOpenAIConnection(config);
29
+ }
30
+ return testAnthropicConnection(config);
31
+ }
32
+
33
+ /**
34
+ * Anthropic-kind connection test (unchanged from pre-kind behavior).
35
+ * @private
36
+ */
37
+ async function testAnthropicConnection(config) {
13
38
  const { baseUrl, authToken, defaultSonnetModel, apiTimeoutMs } = config;
14
39
 
15
40
  try {
16
- // Build client options
17
41
  const clientOptions = {};
18
42
 
19
- if (baseUrl) {
20
- clientOptions.baseURL = baseUrl;
21
- }
22
- if (authToken) {
23
- clientOptions.apiKey = authToken;
24
- }
25
- if (apiTimeoutMs) {
26
- clientOptions.timeout = apiTimeoutMs;
27
- }
43
+ if (baseUrl) clientOptions.baseURL = baseUrl;
44
+ if (authToken) clientOptions.apiKey = authToken;
45
+ if (apiTimeoutMs) clientOptions.timeout = apiTimeoutMs;
28
46
 
29
47
  const client = new Anthropic(clientOptions);
30
48
 
31
- // Use a minimal message to test connectivity
32
- // This verifies: network, auth, and model availability
49
+ // Use a minimal message to test connectivity.
50
+ // This verifies: network, auth, and model availability.
33
51
  const testModel = defaultSonnetModel || 'claude-sonnet-4-20250514';
34
52
 
35
53
  const response = await client.messages.create({
@@ -58,6 +76,88 @@ export async function testProviderConnection(config) {
58
76
  }
59
77
  }
60
78
 
79
+ /**
80
+ * OpenAI-kind connection test. Tries `models.list()` first; if the endpoint
81
+ * does not implement that (common for chat-only proxies like LM Studio), falls
82
+ * back to a minimal `chat.completions.create({ max_tokens: 1 })`.
83
+ * @private
84
+ */
85
+ async function testOpenAIConnection(config) {
86
+ try {
87
+ const client = createOpenAIClient(config);
88
+ return await testOpenAIClient(client, config);
89
+ } catch (error) {
90
+ return failureResponse(error);
91
+ }
92
+ }
93
+
94
+ function createOpenAIClient(config) {
95
+ const { baseUrl, authToken, apiTimeoutMs } = config;
96
+ const clientOptions = { apiKey: authToken || 'missing' };
97
+ if (baseUrl) clientOptions.baseURL = baseUrl;
98
+ if (apiTimeoutMs) clientOptions.timeout = apiTimeoutMs;
99
+ return new OpenAI(clientOptions);
100
+ }
101
+
102
+ async function testOpenAIClient(client, config) {
103
+ try {
104
+ return await testOpenAIModelsEndpoint(client, config);
105
+ } catch (error) {
106
+ if (error?.status !== 404) throw error;
107
+ return testOpenAIChatEndpoint(client, config);
108
+ }
109
+ }
110
+
111
+ async function testOpenAIModelsEndpoint(client, config) {
112
+ const listResult = await client.models.list();
113
+ const first = pickFirstModel(listResult) || config.defaultSonnetModel || null;
114
+ return connectionSuccess(first ? { model: first } : {});
115
+ }
116
+
117
+ async function testOpenAIChatEndpoint(client, config) {
118
+ const testModel = config.defaultSonnetModel || 'gpt-4o-mini';
119
+ const response = await client.chat.completions.create({
120
+ model: testModel,
121
+ max_tokens: 1,
122
+ messages: [{ role: 'user', content: 'Hi' }],
123
+ });
124
+ return connectionSuccess({
125
+ model: response?.model || testModel,
126
+ ...(response?.usage ? { usage: response.usage } : {}),
127
+ });
128
+ }
129
+
130
+ function connectionSuccess(details) {
131
+ return {
132
+ success: true,
133
+ message: 'Connection successful',
134
+ details,
135
+ };
136
+ }
137
+
138
+ function failureResponse(error) {
139
+ return {
140
+ success: false,
141
+ message: getErrorMessage(error),
142
+ details: { code: error.status || error.code, type: error.type || error.name },
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Extract a representative model ID from whatever shape `models.list()` returns.
148
+ * @private
149
+ */
150
+ function pickFirstModel(listResult) {
151
+ if (!listResult) return null;
152
+ // Newer SDKs expose .data; older ones are plain arrays / async iterables.
153
+ const data = Array.isArray(listResult) ? listResult : listResult.data;
154
+ if (Array.isArray(data) && data.length > 0) {
155
+ const entry = data[0];
156
+ return entry?.id || entry?.model || null;
157
+ }
158
+ return null;
159
+ }
160
+
61
161
  /**
62
162
  * Get a human-readable error message from an error object
63
163
  * @param {Error} error - The error object
@@ -0,0 +1,38 @@
1
+ import { resolveAgentTypeFromModel } from './sessionProvider.js';
2
+
3
+ // Human-readable labels used in the cross-kind switch error message.
4
+ export const AGENT_TYPE_LABELS = Object.freeze({
5
+ 'claude-code': 'Claude Code',
6
+ codex: 'Codex',
7
+ });
8
+
9
+ /**
10
+ * @param {string} agentType
11
+ * @returns {string}
12
+ */
13
+ export function agentLabel(agentType) {
14
+ return AGENT_TYPE_LABELS[agentType] || agentType || 'unknown';
15
+ }
16
+
17
+ /**
18
+ * Cross-kind model switch guard. A session is bound to one agent type for its
19
+ * lifetime. If the caller selected a model that resolves to a different agent
20
+ * than the session's existing agentType, reject BEFORE dispatching
21
+ * continueSession or updating session.model — mixing Claude and Codex
22
+ * mid-conversation would produce a broken resume/context state. Same-kind
23
+ * model changes (sonnet↔opus, gpt-4o↔o1-mini) pass through.
24
+ *
25
+ * @param {Object} session - The session row (must include agentType + model).
26
+ * @param {string|null} requestedModel - Model ID from req.body.model, or null.
27
+ * @returns {{ error: string, message: string }|null} 400-body on block, or null to allow.
28
+ */
29
+ export function checkCrossKindSwitch(session, requestedModel) {
30
+ const sessionAgentType = session.agentType || 'claude-code';
31
+ const effectiveModel = requestedModel || session.model;
32
+ const requestedAgentType = resolveAgentTypeFromModel(effectiveModel);
33
+ if (requestedAgentType === sessionAgentType) return null;
34
+ return {
35
+ error: 'CROSS_KIND_MODEL_SWITCH',
36
+ message: `Cannot switch agent kind mid-session (${agentLabel(sessionAgentType)} → ${agentLabel(requestedAgentType)})`,
37
+ };
38
+ }