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
@@ -12,6 +12,7 @@ const UPSERT_FIELD_MAP = [
12
12
  { key: 'gitMode', column: 'git_mode' },
13
13
  { key: 'gitBranch', column: 'git_branch' },
14
14
  { key: 'model', column: 'model' },
15
+ { key: 'providerId', column: 'provider_id' },
15
16
  { key: 'effortLevel', column: 'effort_level' },
16
17
  ];
17
18
 
@@ -47,6 +48,7 @@ export class ProjectDefaultsRepository extends BaseRepository {
47
48
  gitMode: row.git_mode || null,
48
49
  gitBranch: row.git_branch || null,
49
50
  model: row.model || null,
51
+ providerId: row.provider_id || null,
50
52
  effortLevel: row.effort_level || null,
51
53
  createdAt: row.created_at,
52
54
  updatedAt: row.updated_at,
@@ -72,47 +74,53 @@ export class ProjectDefaultsRepository extends BaseRepository {
72
74
  * @param {Object} data - Defaults data (all fields optional)
73
75
  * @returns {Object} Updated defaults object
74
76
  */
77
+ #insertDefaults(projectId, data) {
78
+ const id = databaseManager.generateId();
79
+ const now = Date.now();
80
+
81
+ this.db
82
+ .prepare(
83
+ `INSERT INTO project_session_defaults
84
+ (id, project_id, mode, thinking_enabled, start_immediately, git_mode, git_branch, model, provider_id, effort_level, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
86
+ )
87
+ .run(
88
+ id,
89
+ projectId,
90
+ data.mode || null,
91
+ data.thinkingEnabled !== undefined ? (data.thinkingEnabled ? 1 : 0) : null,
92
+ data.startImmediately !== undefined ? (data.startImmediately ? 1 : 0) : null,
93
+ data.gitMode || null,
94
+ data.gitBranch || null,
95
+ data.model || null,
96
+ data.providerId || null,
97
+ data.effortLevel || null,
98
+ now,
99
+ now
100
+ );
101
+ }
102
+
103
+ #updateDefaults(projectId, data) {
104
+ const { updates, values } = buildUpdateFields(data);
105
+
106
+ if (updates.length > 0) {
107
+ updates.push('updated_at = ?');
108
+ values.push(Date.now());
109
+ values.push(projectId);
110
+
111
+ this.db
112
+ .prepare(`UPDATE project_session_defaults SET ${updates.join(', ')} WHERE project_id = ?`)
113
+ .run(...values);
114
+ }
115
+ }
116
+
75
117
  upsert(projectId, data) {
76
- // Get existing defaults if any
77
118
  const existing = this.getByProjectId(projectId);
78
119
 
79
120
  if (!existing) {
80
- // Create new defaults record
81
- const id = databaseManager.generateId();
82
- const now = Date.now();
83
-
84
- this.db
85
- .prepare(
86
- `INSERT INTO project_session_defaults
87
- (id, project_id, mode, thinking_enabled, start_immediately, git_mode, git_branch, model, effort_level, created_at, updated_at)
88
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
89
- )
90
- .run(
91
- id,
92
- projectId,
93
- data.mode || null,
94
- data.thinkingEnabled !== undefined ? (data.thinkingEnabled ? 1 : 0) : null,
95
- data.startImmediately !== undefined ? (data.startImmediately ? 1 : 0) : null,
96
- data.gitMode || null,
97
- data.gitBranch || null,
98
- data.model || null,
99
- data.effortLevel || null,
100
- now,
101
- now
102
- );
121
+ this.#insertDefaults(projectId, data);
103
122
  } else {
104
- // Update existing defaults
105
- const { updates, values } = buildUpdateFields(data);
106
-
107
- if (updates.length > 0) {
108
- updates.push('updated_at = ?');
109
- values.push(Date.now());
110
- values.push(projectId);
111
-
112
- this.db
113
- .prepare(`UPDATE project_session_defaults SET ${updates.join(', ')} WHERE project_id = ?`)
114
- .run(...values);
115
- }
123
+ this.#updateDefaults(projectId, data);
116
124
  }
117
125
 
118
126
  return this.getByProjectId(projectId);
@@ -137,7 +145,8 @@ export class ProjectDefaultsRepository extends BaseRepository {
137
145
  .prepare(
138
146
  `UPDATE project_session_defaults
139
147
  SET mode = NULL, thinking_enabled = NULL, start_immediately = NULL,
140
- git_mode = NULL, git_branch = NULL, model = NULL, effort_level = NULL, updated_at = ?
148
+ git_mode = NULL, git_branch = NULL, model = NULL, provider_id = NULL,
149
+ effort_level = NULL, updated_at = ?
141
150
  WHERE project_id = ?`
142
151
  )
143
152
  .run(Date.now(), projectId);
@@ -167,6 +176,7 @@ export class ProjectDefaultsRepository extends BaseRepository {
167
176
  gitMode: null,
168
177
  gitBranch: null,
169
178
  model: null,
179
+ providerId: null,
170
180
  effortLevel: null,
171
181
  };
172
182
  }
@@ -2,6 +2,22 @@ import { BaseRepository } from './BaseRepository.js';
2
2
  import { databaseManager } from './DatabaseManager.js';
3
3
  import { encrypt, decrypt } from '../services/encryption.js';
4
4
 
5
+ /**
6
+ * Valid values for `providers.kind`. Maps 1:1 to an agent adapter:
7
+ * - 'anthropic' → 'claude-code'
8
+ * - 'openai' → 'codex'
9
+ */
10
+ export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
11
+
12
+ /**
13
+ * Mapping from provider kind to the agent adapter that should drive sessions
14
+ * backed by that provider.
15
+ */
16
+ export const AGENT_TYPE_BY_KIND = Object.freeze({
17
+ anthropic: 'claude-code',
18
+ openai: 'codex',
19
+ });
20
+
5
21
  /**
6
22
  * Provider repository class (replaces ModelProviderRepository).
7
23
  *
@@ -11,6 +27,8 @@ import { encrypt, decrypt } from '../services/encryption.js';
11
27
  * - No auto-sync logic (#syncDefaultModels removed)
12
28
  * - Auth tokens are encrypted at rest (AES-256-GCM via encryption service)
13
29
  * - `getProviderByModelId` includes models (needed for buildProviderEnv)
30
+ * - Providers carry a `kind` (`'anthropic'` | `'openai'`) that selects the
31
+ * agent adapter and env-var convention. `kind` is **immutable** after create.
14
32
  */
15
33
  export class ProviderRepository extends BaseRepository {
16
34
  constructor() {
@@ -27,6 +45,7 @@ export class ProviderRepository extends BaseRepository {
27
45
  apiTimeoutMs: row.api_timeout_ms,
28
46
  additionalEnvVars: row.additional_env_vars ? JSON.parse(row.additional_env_vars) : null,
29
47
  isBuiltIn: row.is_built_in === 1,
48
+ kind: row.kind || 'anthropic',
30
49
  createdAt: row.created_at,
31
50
  updatedAt: row.updated_at,
32
51
  };
@@ -64,12 +83,20 @@ export class ProviderRepository extends BaseRepository {
64
83
  authToken = null,
65
84
  apiTimeoutMs = null,
66
85
  additionalEnvVars = null,
86
+ kind = 'anthropic',
67
87
  } = data;
68
88
 
89
+ // Application-layer validation: give a clear error ahead of the DB CHECK.
90
+ if (!PROVIDER_KINDS.includes(kind)) {
91
+ throw new Error(
92
+ `Invalid provider kind "${kind}". Must be one of: ${PROVIDER_KINDS.join(', ')}.`
93
+ );
94
+ }
95
+
69
96
  this.db
70
97
  .prepare(
71
- `INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, created_at, updated_at)
72
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
98
+ `INSERT INTO providers (id, name, base_url, auth_token, api_timeout_ms, additional_env_vars, kind, created_at, updated_at)
99
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
73
100
  )
74
101
  .run(
75
102
  id,
@@ -78,6 +105,7 @@ export class ProviderRepository extends BaseRepository {
78
105
  encrypt(authToken),
79
106
  apiTimeoutMs,
80
107
  additionalEnvVars ? JSON.stringify(additionalEnvVars) : null,
108
+ kind,
81
109
  now,
82
110
  now
83
111
  );
@@ -115,6 +143,15 @@ export class ProviderRepository extends BaseRepository {
115
143
  * @returns {Object} Updated provider (with models array)
116
144
  */
117
145
  update(id, data) {
146
+ // `kind` is immutable after create. Existing models + env wiring depend on it,
147
+ // so changing it in place would silently corrupt sessions already attached to
148
+ // this provider.
149
+ if (data && Object.prototype.hasOwnProperty.call(data, 'kind')) {
150
+ throw new Error(
151
+ "Provider kind is immutable after create. Delete and recreate the provider to change kind."
152
+ );
153
+ }
154
+
118
155
  const updates = [];
119
156
  const values = [];
120
157
 
@@ -279,12 +316,15 @@ export class ProviderRepository extends BaseRepository {
279
316
  return null;
280
317
  }
281
318
 
282
- // Find which provider owns this model ID
319
+ // Prefer custom providers over built-ins for duplicate model IDs. This
320
+ // preserves user-managed OpenAI providers (alternate base URLs, keys, or
321
+ // env vars) even when official OpenAI models are also seeded built-ins.
283
322
  const row = this.db
284
323
  .prepare(
285
324
  `SELECT p.id FROM providers p
286
325
  JOIN provider_models pm ON p.id = pm.provider_id
287
- WHERE pm.model_id = ?`
326
+ WHERE pm.model_id = ?
327
+ ORDER BY p.is_built_in ASC, p.name ASC`
288
328
  )
289
329
  .get(modelId);
290
330
 
@@ -297,11 +337,27 @@ export class ProviderRepository extends BaseRepository {
297
337
  const provider = this.getById(row.id);
298
338
  if (!provider) return null;
299
339
 
300
- // Built-in Anthropic provider falls through to SDK defaults
301
- if (provider.isBuiltIn) {
340
+ // Built-in **Anthropic** provider falls through to SDK defaults (keeps
341
+ // historical behavior of letting @anthropic-ai/claude-agent-sdk pick its
342
+ // own env). Built-in OpenAI (or any future non-Anthropic built-in) still
343
+ // needs its env vars to flow, so we return the provider object.
344
+ if (provider.isBuiltIn && provider.kind === 'anthropic') {
302
345
  return null;
303
346
  }
304
347
 
305
348
  return provider;
306
349
  }
350
+
351
+ /**
352
+ * Resolve a provider's agent type from its id.
353
+ * @param {string|null|undefined} providerId
354
+ * @returns {string|null} 'claude-code' for anthropic-kind, 'codex' for openai-kind,
355
+ * or null if the provider is unknown.
356
+ */
357
+ getAgentTypeForProvider(providerId) {
358
+ if (!providerId) return null;
359
+ const provider = this.getById(providerId);
360
+ if (!provider) return null;
361
+ return AGENT_TYPE_BY_KIND[provider.kind] || null;
362
+ }
307
363
  }
@@ -1,6 +1,6 @@
1
1
  import { BaseRepository } from './BaseRepository.js';
2
2
  import { databaseManager } from './DatabaseManager.js';
3
- import { messages, conversations } from './index.js';
3
+ import { messages, conversations, modelProviders } from './index.js';
4
4
  import {
5
5
  ACTIVITY_FIELDS_SQL,
6
6
  mapTokenUsage,
@@ -9,6 +9,26 @@ import {
9
9
  buildUpdateClauses,
10
10
  } from './session-helpers.js';
11
11
 
12
+ const DEFAULT_AGENT_TYPE = 'claude-code';
13
+
14
+ /**
15
+ * Resolve the agent type ('claude-code' or 'codex') from a model ID by looking
16
+ * up which provider owns the model. This is the same logic as
17
+ * sessionProvider.resolveAgentTypeFromModel but inlined here to avoid a
18
+ * circular dependency:
19
+ * database.js (index) → SessionRepository → sessionProvider → database.js
20
+ * @param {string|null} modelId
21
+ * @returns {'claude-code'|'codex'}
22
+ */
23
+ function resolveAgentTypeFromModel(modelId) {
24
+ if (!modelId) return DEFAULT_AGENT_TYPE;
25
+ const provider = modelProviders.getProviderByModelId(modelId);
26
+ if (!provider) return DEFAULT_AGENT_TYPE;
27
+ // ProviderRepository.getAgentTypeForProvider maps kind → agent adapter
28
+ const agentType = modelProviders.getAgentTypeForProvider(provider.id);
29
+ return agentType || DEFAULT_AGENT_TYPE;
30
+ }
31
+
12
32
  /**
13
33
  * Session repository class
14
34
  */
@@ -25,6 +45,7 @@ export class SessionRepository extends BaseRepository {
25
45
  status: row.status,
26
46
  mode: row.mode,
27
47
  model: row.model,
48
+ providerId: row.provider_id || null,
28
49
  thinkingEnabled: Boolean(row.thinking_enabled),
29
50
  archived: Boolean(row.archived),
30
51
  starred: Boolean(row.starred),
@@ -42,6 +63,8 @@ export class SessionRepository extends BaseRepository {
42
63
  effortLevel: row.effort_level || null,
43
64
  autoSendPendingPrompt: Boolean(row.auto_send_pending_prompt),
44
65
  slashCommands: row.slash_commands || null,
66
+ // Agent runtime driving this session (fallback to 'claude-code' for legacy rows).
67
+ agentType: row.agent_type || DEFAULT_AGENT_TYPE,
45
68
  ...mapTokenUsage(row),
46
69
  ...mapScheduling(row),
47
70
  // Kanban fields
@@ -49,7 +72,7 @@ export class SessionRepository extends BaseRepository {
49
72
  laneTriggerDepth: row.lane_trigger_depth || 0,
50
73
  createdAt: row.created_at,
51
74
  updatedAt: row.updated_at,
52
- lastActivityAt: row.last_activity_at || row.updated_at || row.created_at,
75
+ lastActivityAt: row.last_activity_at ?? null,
53
76
  activeTimeMs: row.active_time_ms || 0,
54
77
  };
55
78
  }
@@ -62,18 +85,39 @@ export class SessionRepository extends BaseRepository {
62
85
  return this.map(row);
63
86
  }
64
87
 
65
- /** Create a new session with optional config (mode, thinkingEnabled, gitBranch, parentSessionId, status, model, effortLevel) */
88
+ /** Create a new session with optional config (mode, thinkingEnabled, gitBranch, parentSessionId, status, model, providerId, effortLevel, agentType) */
66
89
  create(projectId, name, prompt, options = {}) {
67
90
  const config = parseCreateConfig(options, Array.prototype.slice.call(arguments, 4));
68
91
 
92
+ // Resolve agentType: explicit override → model-based derivation → fallback
93
+ const agentType =
94
+ config.agentType
95
+ ?? (config.model ? resolveAgentTypeFromModel(config.model) : null)
96
+ ?? DEFAULT_AGENT_TYPE;
97
+
69
98
  const id = databaseManager.generateId();
70
99
  const now = Date.now();
71
100
  this.db
72
101
  .prepare(
73
- `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, parent_session_id, model, effort_level, created_at, updated_at)
74
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
102
+ `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, parent_session_id, model, provider_id, effort_level, agent_type, created_at, updated_at)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
75
104
  )
76
- .run(id, projectId, name, config.status, config.mode, config.thinkingEnabled ? 1 : 0, config.gitBranch, config.parentSessionId, config.model, config.effortLevel, now, now);
105
+ .run(
106
+ id,
107
+ projectId,
108
+ name,
109
+ config.status,
110
+ config.mode,
111
+ config.thinkingEnabled ? 1 : 0,
112
+ config.gitBranch,
113
+ config.parentSessionId,
114
+ config.model,
115
+ config.providerId,
116
+ config.effortLevel,
117
+ agentType,
118
+ now,
119
+ now
120
+ );
77
121
 
78
122
  // Create initial conversation
79
123
  const conversation = conversations.create(id, 'Initial', true);
@@ -103,6 +147,7 @@ export class SessionRepository extends BaseRepository {
103
147
 
104
148
  sql += ` ORDER BY
105
149
  starred DESC,
150
+ COALESCE(last_activity_at, updated_at, created_at) DESC,
106
151
  updated_at DESC,
107
152
  created_at DESC,
108
153
  rowid DESC`;
@@ -141,7 +186,7 @@ export class SessionRepository extends BaseRepository {
141
186
  `SELECT s.*, p.name as project_name, p.working_directory as project_working_directory, ${ACTIVITY_FIELDS_SQL}
142
187
  FROM sessions s JOIN projects p ON s.project_id = p.id
143
188
  WHERE s.status IN ('starting', 'running', 'waiting') AND s.archived = 0
144
- ORDER BY s.starred DESC, s.updated_at DESC, s.created_at DESC, s.rowid DESC`
189
+ ORDER BY s.starred DESC, COALESCE(last_activity_at, s.updated_at, s.created_at) DESC, s.updated_at DESC, s.created_at DESC, s.rowid DESC`
145
190
  )
146
191
  .all();
147
192
  return rows.map(row => ({
@@ -157,7 +202,7 @@ export class SessionRepository extends BaseRepository {
157
202
  .prepare(
158
203
  `SELECT s.*, ${ACTIVITY_FIELDS_SQL} FROM sessions s
159
204
  WHERE parent_session_id = ?
160
- ORDER BY updated_at DESC, created_at DESC, rowid DESC`
205
+ ORDER BY COALESCE(last_activity_at, updated_at, created_at) DESC, updated_at DESC, created_at DESC, rowid DESC`
161
206
  )
162
207
  .all(parentSessionId);
163
208
  return this.mapAll(rows);
@@ -213,6 +258,18 @@ export class SessionRepository extends BaseRepository {
213
258
  return this.getById(id);
214
259
  }
215
260
 
261
+ /**
262
+ * Touch a session to update its updated_at timestamp without changing other fields.
263
+ * This is used to mark a session as recently active (e.g., when a message is added).
264
+ * @param {string} id - Session ID
265
+ * @returns {Object|null} The updated session or null if not found
266
+ */
267
+ touch(id) {
268
+ const now = Date.now();
269
+ this.db.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run(now, id);
270
+ return this.getById(id);
271
+ }
272
+
216
273
  /** Duplicate a session with a new ID and reset state (does NOT handle git or conversation setup) */
217
274
  duplicate(sourceSessionId, { name } = {}) {
218
275
  const source = this.getById(sourceSessionId);
@@ -227,10 +284,10 @@ export class SessionRepository extends BaseRepository {
227
284
  // Insert new session with same settings but new ID and status
228
285
  this.db
229
286
  .prepare(
230
- `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, model, effort_level, context_window,
231
- input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens,
287
+ `INSERT INTO sessions (id, project_id, name, status, mode, thinking_enabled, git_branch, model, provider_id, effort_level, agent_type, context_window,
288
+ input_tokens, output_tokens, thinking_tokens, cache_read_input_tokens, cache_creation_input_tokens,
232
289
  web_search_requests, cost_usd, created_at, updated_at)
233
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
234
291
  )
235
292
  .run(
236
293
  id,
@@ -241,10 +298,13 @@ export class SessionRepository extends BaseRepository {
241
298
  source.thinkingEnabled ? 1 : 0,
242
299
  source.gitBranch, // Copy branch name (NOT worktree path)
243
300
  source.model,
301
+ source.providerId,
244
302
  source.effortLevel,
303
+ source.agentType || DEFAULT_AGENT_TYPE,
245
304
  source.contextWindow,
246
305
  source.inputTokens,
247
306
  source.outputTokens,
307
+ source.thinkingTokens,
248
308
  source.cacheReadInputTokens,
249
309
  source.cacheCreationInputTokens,
250
310
  source.webSearchRequests,
@@ -261,7 +321,7 @@ export class SessionRepository extends BaseRepository {
261
321
  const rows = this.db
262
322
  .prepare(
263
323
  `SELECT s.*, ${ACTIVITY_FIELDS_SQL} FROM sessions s
264
- WHERE pr_url IS NOT NULL ORDER BY updated_at DESC, created_at DESC, rowid DESC`
324
+ WHERE pr_url IS NOT NULL ORDER BY COALESCE(last_activity_at, updated_at, created_at) DESC, updated_at DESC, created_at DESC, rowid DESC`
265
325
  )
266
326
  .all();
267
327
  return this.mapAll(rows);
@@ -270,11 +330,11 @@ export class SessionRepository extends BaseRepository {
270
330
  updateUsage(id, usage) {
271
331
  this.db
272
332
  .prepare(
273
- `UPDATE sessions SET input_tokens = ?, output_tokens = ?, cache_read_input_tokens = ?,
333
+ `UPDATE sessions SET input_tokens = ?, output_tokens = ?, thinking_tokens = ?, cache_read_input_tokens = ?,
274
334
  cache_creation_input_tokens = ?, web_search_requests = ?, context_window = ?, updated_at = ?
275
335
  WHERE id = ?`
276
336
  )
277
- .run(usage.inputTokens, usage.outputTokens, usage.cacheReadInputTokens,
337
+ .run(usage.inputTokens, usage.outputTokens, usage.thinkingTokens || 0, usage.cacheReadInputTokens,
278
338
  usage.cacheCreationInputTokens, usage.webSearchRequests, usage.contextWindow, Date.now(), id);
279
339
  return this.getById(id);
280
340
  }
@@ -5,6 +5,13 @@ const TOKEN_WEIGHTS_KEY = 'token_cost_weights';
5
5
  const SUMMARY_SETTINGS_KEY = 'summary_settings';
6
6
  const GENERAL_SETTINGS_KEY = 'general_settings';
7
7
 
8
+ const DEFAULT_SUMMARY_SETTINGS = Object.freeze({
9
+ disableSessionSummaries: false,
10
+ sessionTitlePrompt: '',
11
+ summaryModel: '',
12
+ summaryProviderId: null,
13
+ });
14
+
8
15
  /**
9
16
  * Settings repository for managing application-wide settings
10
17
  */
@@ -114,22 +121,13 @@ export class SettingsRepository {
114
121
  getSummarySettings() {
115
122
  const value = this.get(SUMMARY_SETTINGS_KEY);
116
123
  if (!value) {
117
- return {
118
- disableSessionSummaries: false,
119
- sessionTitlePrompt: '',
120
- };
124
+ return { ...DEFAULT_SUMMARY_SETTINGS };
121
125
  }
122
126
  try {
123
127
  const parsed = JSON.parse(value);
124
- return {
125
- disableSessionSummaries: parsed.disableSessionSummaries || false,
126
- sessionTitlePrompt: parsed.sessionTitlePrompt || '',
127
- };
128
+ return normalizeStoredSummarySettings(parsed);
128
129
  } catch {
129
- return {
130
- disableSessionSummaries: false,
131
- sessionTitlePrompt: '',
132
- };
130
+ return { ...DEFAULT_SUMMARY_SETTINGS };
133
131
  }
134
132
  }
135
133
 
@@ -138,11 +136,22 @@ export class SettingsRepository {
138
136
  * @param {Object} settings - Summary settings
139
137
  * @param {boolean} settings.disableSessionSummaries - Disable session summaries
140
138
  * @param {string} settings.sessionTitlePrompt - Custom session title prompt
139
+ * @param {string} [settings.summaryModel] - Summary model id; empty string means auto
140
+ * @param {string|null} [settings.summaryProviderId] - Provider id owning summaryModel
141
141
  */
142
142
  setSummarySettings(settings) {
143
+ const summaryModel = String(settings.summaryModel || '');
144
+ const summaryProviderId = typeof settings.summaryProviderId === 'string'
145
+ ? settings.summaryProviderId
146
+ : null;
147
+ if (summaryModel && !summaryProviderId) {
148
+ throw new Error('summaryProviderId is required when summaryModel is set');
149
+ }
143
150
  const validated = {
144
151
  disableSessionSummaries: Boolean(settings.disableSessionSummaries),
145
152
  sessionTitlePrompt: String(settings.sessionTitlePrompt || ''),
153
+ summaryModel,
154
+ summaryProviderId: summaryModel ? summaryProviderId : null,
146
155
  };
147
156
  this.set(SUMMARY_SETTINGS_KEY, JSON.stringify(validated));
148
157
  return validated;
@@ -154,10 +163,7 @@ export class SettingsRepository {
154
163
  */
155
164
  resetSummarySettings() {
156
165
  this.delete(SUMMARY_SETTINGS_KEY);
157
- return {
158
- disableSessionSummaries: false,
159
- sessionTitlePrompt: '',
160
- };
166
+ return { ...DEFAULT_SUMMARY_SETTINGS };
161
167
  }
162
168
 
163
169
  // General Settings
@@ -209,3 +215,25 @@ export class SettingsRepository {
209
215
  };
210
216
  }
211
217
  }
218
+
219
+ function normalizeStoredSummarySettings(parsed) {
220
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
221
+ return { ...DEFAULT_SUMMARY_SETTINGS };
222
+ }
223
+
224
+ const summaryModel = typeof parsed.summaryModel === 'string' ? parsed.summaryModel : '';
225
+ const summaryProviderId = typeof parsed.summaryProviderId === 'string' && parsed.summaryProviderId
226
+ ? parsed.summaryProviderId
227
+ : null;
228
+
229
+ if (summaryModel && !summaryProviderId) {
230
+ return { ...DEFAULT_SUMMARY_SETTINGS };
231
+ }
232
+
233
+ return {
234
+ disableSessionSummaries: Boolean(parsed.disableSessionSummaries),
235
+ sessionTitlePrompt: typeof parsed.sessionTitlePrompt === 'string' ? parsed.sessionTitlePrompt : '',
236
+ summaryModel,
237
+ summaryProviderId: summaryModel ? summaryProviderId : null,
238
+ };
239
+ }
@@ -22,6 +22,7 @@ export function mapConversationRow(row) {
22
22
  // Token usage fields
23
23
  inputTokens: row.input_tokens || 0,
24
24
  outputTokens: row.output_tokens || 0,
25
+ thinkingTokens: row.thinking_tokens || 0,
25
26
  cacheReadInputTokens: row.cache_read_input_tokens || 0,
26
27
  cacheCreationInputTokens: row.cache_creation_input_tokens || 0,
27
28
  webSearchRequests: row.web_search_requests || 0,
@@ -113,6 +113,10 @@ export const conversationsMigrations = [
113
113
  name: 'conversations-add-output_tokens',
114
114
  up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
115
115
  },
116
+ {
117
+ name: 'conversations-add-thinking_tokens',
118
+ up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
119
+ },
116
120
  {
117
121
  name: 'conversations-add-cache_read_input_tokens',
118
122
  up(db) { addColumnIfMissing(db, TABLE_CONVERSATIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
@@ -114,6 +114,7 @@ export const allMigrations = validateMigrations([
114
114
  c.get('conversations-add-claude_session_id'),
115
115
  c.get('conversations-add-input_tokens'),
116
116
  c.get('conversations-add-output_tokens'),
117
+ c.get('conversations-add-thinking_tokens'),
117
118
  c.get('conversations-add-cache_read_input_tokens'),
118
119
  c.get('conversations-add-cache_creation_input_tokens'),
119
120
  c.get('conversations-add-web_search_requests'),
@@ -123,6 +124,7 @@ export const allMigrations = validateMigrations([
123
124
  // --- Sessions token usage ---
124
125
  s.get('sessions-add-input_tokens'),
125
126
  s.get('sessions-add-output_tokens'),
127
+ s.get('sessions-add-thinking_tokens'),
126
128
  s.get('sessions-add-cache_read_input_tokens'),
127
129
  s.get('sessions-add-cache_creation_input_tokens'),
128
130
  s.get('sessions-add-web_search_requests'),
@@ -168,7 +170,9 @@ export const allMigrations = validateMigrations([
168
170
 
169
171
  // --- Providers + provider_models tables + seed ---
170
172
  m.get('providers-create-tables'),
173
+ m.get('providers-add-kind'),
171
174
  m.get('providers-seed-built-in'),
175
+ m.get('providers-seed-built-in-openai'),
172
176
 
173
177
  // --- Sessions provider_id (from providers FK) ---
174
178
  s.get('sessions-add-provider_id-from-providers'),
@@ -4,12 +4,13 @@
4
4
  * Each export is an array of { name, up(db) } migration objects.
5
5
  */
6
6
  import { randomUUID } from 'node:crypto';
7
+ import { OPENAI_MODELS } from '../../../../shared/src/index.js';
7
8
  import { addColumnIfMissing, tableExists } from './migrationUtils.js';
8
9
 
9
10
  /**
10
11
  * Seed the built-in Anthropic provider if it doesn't exist.
11
12
  */
12
- function seedBuiltInProvider(db) {
13
+ function seedBuiltInAnthropicProvider(db) {
13
14
  const providerId = 'anthropic-default';
14
15
 
15
16
  const existing = db
@@ -43,6 +44,35 @@ function seedBuiltInProvider(db) {
43
44
  }
44
45
  }
45
46
 
47
+ /**
48
+ * Seed the built-in OpenAI/Codex provider if it doesn't exist.
49
+ */
50
+ function seedBuiltInOpenAIProvider(db) {
51
+ const providerId = 'openai-default';
52
+ const now = Date.now();
53
+
54
+ db.prepare(
55
+ `INSERT OR IGNORE INTO providers (
56
+ id, name, base_url, auth_token, kind, is_built_in, created_at, updated_at
57
+ )
58
+ VALUES (?, ?, NULL, NULL, 'openai', 1, ?, ?)`
59
+ ).run(providerId, 'OpenAI (Official)', now, now);
60
+
61
+ const insertModel = db.prepare(
62
+ `INSERT OR IGNORE INTO provider_models (id, provider_id, model_id, display_name, description, tier, created_at)
63
+ VALUES (?, ?, ?, ?, ?, 'custom', ?)`
64
+ );
65
+
66
+ for (const model of OPENAI_MODELS) {
67
+ insertModel.run(model.seedId, providerId, model.id, model.name, model.description, now);
68
+ }
69
+ }
70
+
71
+ function seedBuiltInProviders(db) {
72
+ seedBuiltInAnthropicProvider(db);
73
+ seedBuiltInOpenAIProvider(db);
74
+ }
75
+
46
76
  /**
47
77
  * Update built-in models to 4.6 versions.
48
78
  */
@@ -217,6 +247,7 @@ export const miscMigrations = [
217
247
  api_timeout_ms INTEGER,
218
248
  additional_env_vars TEXT,
219
249
  is_built_in INTEGER NOT NULL DEFAULT 0,
250
+ kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
220
251
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
221
252
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
222
253
  );
@@ -235,10 +266,29 @@ export const miscMigrations = [
235
266
  },
236
267
  },
237
268
 
238
- // --- Seed built-in provider ---
269
+ // --- Add `kind` column to providers (for DBs that predate the CREATE TABLE change) ---
270
+ {
271
+ name: 'providers-add-kind',
272
+ up(db) {
273
+ addColumnIfMissing(
274
+ db,
275
+ 'providers',
276
+ 'kind',
277
+ "TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai'))"
278
+ );
279
+ },
280
+ },
281
+
282
+ // --- Seed built-in providers ---
239
283
  {
240
284
  name: 'providers-seed-built-in',
241
- up(db) { seedBuiltInProvider(db); },
285
+ up(db) { seedBuiltInProviders(db); },
286
+ },
287
+
288
+ // --- Seed built-in OpenAI provider for DBs that already ran the original seed ---
289
+ {
290
+ name: 'providers-seed-built-in-openai',
291
+ up(db) { seedBuiltInOpenAIProvider(db); },
242
292
  },
243
293
 
244
294
  // --- Update built-in models to 4.6 ---