devchain-cli 0.12.1 → 0.12.3

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 (106) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +154 -33
  3. package/dist/drizzle/0060_huge_proemial_gods.sql +12 -0
  4. package/dist/drizzle/0061_easy_silver_surfer.sql +2 -0
  5. package/dist/drizzle/meta/0060_snapshot.json +5170 -0
  6. package/dist/drizzle/meta/0061_snapshot.json +5163 -0
  7. package/dist/drizzle/meta/_journal.json +14 -0
  8. package/dist/server/common/config/env.config.d.ts +1 -1
  9. package/dist/server/common/config/env.config.js +9 -1
  10. package/dist/server/common/config/env.config.js.map +1 -1
  11. package/dist/server/common/config/host-helpers.d.ts +7 -0
  12. package/dist/server/common/config/host-helpers.js +34 -0
  13. package/dist/server/common/config/host-helpers.js.map +1 -0
  14. package/dist/server/main.js +5 -3
  15. package/dist/server/main.js.map +1 -1
  16. package/dist/server/modules/core/core-normal.module.js +3 -2
  17. package/dist/server/modules/core/core-normal.module.js.map +1 -1
  18. package/dist/server/modules/core/services/gemini-trusted-folders.service.d.ts +26 -0
  19. package/dist/server/modules/core/services/gemini-trusted-folders.service.js +181 -0
  20. package/dist/server/modules/core/services/gemini-trusted-folders.service.js.map +1 -0
  21. package/dist/server/modules/core/services/preflight.service.js +2 -1
  22. package/dist/server/modules/core/services/preflight.service.js.map +1 -1
  23. package/dist/server/modules/core/services/provider-mcp-ensure.service.d.ts +10 -1
  24. package/dist/server/modules/core/services/provider-mcp-ensure.service.js +86 -19
  25. package/dist/server/modules/core/services/provider-mcp-ensure.service.js.map +1 -1
  26. package/dist/server/modules/events/catalog/claude.hooks.session.started.d.ts +2 -2
  27. package/dist/server/modules/events/catalog/index.d.ts +20 -2
  28. package/dist/server/modules/events/catalog/index.js +5 -1
  29. package/dist/server/modules/events/catalog/index.js.map +1 -1
  30. package/dist/server/modules/events/catalog/session.restored.d.ts +21 -0
  31. package/dist/server/modules/events/catalog/session.restored.js +14 -0
  32. package/dist/server/modules/events/catalog/session.restored.js.map +1 -0
  33. package/dist/server/modules/hooks/dtos/hook-event.dto.d.ts +2 -2
  34. package/dist/server/modules/mcp/services/mcp-provider-registration.service.d.ts +1 -0
  35. package/dist/server/modules/mcp/services/mcp-provider-registration.service.js +91 -4
  36. package/dist/server/modules/mcp/services/mcp-provider-registration.service.js.map +1 -1
  37. package/dist/server/modules/projects/controllers/projects.controller.d.ts +4 -1
  38. package/dist/server/modules/projects/controllers/projects.controller.js +1 -1
  39. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  40. package/dist/server/modules/projects/projects.module.js +11 -2
  41. package/dist/server/modules/projects/projects.module.js.map +1 -1
  42. package/dist/server/modules/projects/services/project-provider-provisioning.service.d.ts +18 -0
  43. package/dist/server/modules/projects/services/project-provider-provisioning.service.js +92 -0
  44. package/dist/server/modules/projects/services/project-provider-provisioning.service.js.map +1 -0
  45. package/dist/server/modules/projects/services/projects.service.d.ts +9 -2
  46. package/dist/server/modules/projects/services/projects.service.js +31 -4
  47. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  48. package/dist/server/modules/providers/adapters/claude.adapter.d.ts +4 -1
  49. package/dist/server/modules/providers/adapters/claude.adapter.js +6 -0
  50. package/dist/server/modules/providers/adapters/claude.adapter.js.map +1 -1
  51. package/dist/server/modules/providers/adapters/codex.adapter.d.ts +4 -1
  52. package/dist/server/modules/providers/adapters/codex.adapter.js +6 -0
  53. package/dist/server/modules/providers/adapters/codex.adapter.js.map +1 -1
  54. package/dist/server/modules/providers/adapters/gemini.adapter.d.ts +6 -1
  55. package/dist/server/modules/providers/adapters/gemini.adapter.js +14 -3
  56. package/dist/server/modules/providers/adapters/gemini.adapter.js.map +1 -1
  57. package/dist/server/modules/providers/adapters/opencode.adapter.d.ts +4 -1
  58. package/dist/server/modules/providers/adapters/opencode.adapter.js +6 -0
  59. package/dist/server/modules/providers/adapters/opencode.adapter.js.map +1 -1
  60. package/dist/server/modules/providers/adapters/provider-adapter.interface.d.ts +10 -0
  61. package/dist/server/modules/providers/controllers/providers.controller.d.ts +1 -0
  62. package/dist/server/modules/providers/controllers/providers.controller.js +1 -0
  63. package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
  64. package/dist/server/modules/reviews/dtos/review.dto.d.ts +2 -2
  65. package/dist/server/modules/session-reader/data/pricing.json +20 -0
  66. package/dist/server/modules/session-reader/services/transcript-persistence.listener.js +3 -3
  67. package/dist/server/modules/session-reader/services/transcript-persistence.listener.js.map +1 -1
  68. package/dist/server/modules/sessions/controllers/sessions.controller.d.ts +3 -1
  69. package/dist/server/modules/sessions/controllers/sessions.controller.js +57 -0
  70. package/dist/server/modules/sessions/controllers/sessions.controller.js.map +1 -1
  71. package/dist/server/modules/sessions/dtos/sessions.dto.d.ts +25 -1
  72. package/dist/server/modules/sessions/dtos/sessions.dto.js +4 -1
  73. package/dist/server/modules/sessions/dtos/sessions.dto.js.map +1 -1
  74. package/dist/server/modules/sessions/services/sessions.service.d.ts +35 -1
  75. package/dist/server/modules/sessions/services/sessions.service.js +480 -118
  76. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  77. package/dist/server/modules/sessions/utils/env-builder.d.ts +1 -1
  78. package/dist/server/modules/sessions/utils/env-builder.js +3 -3
  79. package/dist/server/modules/sessions/utils/env-builder.js.map +1 -1
  80. package/dist/server/modules/storage/db/schema.d.ts +38 -2
  81. package/dist/server/modules/storage/db/schema.js +4 -1
  82. package/dist/server/modules/storage/db/schema.js.map +1 -1
  83. package/dist/server/modules/subscribers/events/event-fields-catalog.js +12 -0
  84. package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
  85. package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +6 -0
  86. package/dist/server/modules/terminal/gateways/terminal.gateway.js +17 -0
  87. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  88. package/dist/server/templates/3-agents-dev.json +111 -64
  89. package/dist/server/templates/5-agents-dev.json +153 -106
  90. package/dist/server/templates/teams-dev.json +160 -112
  91. package/dist/server/test-setup-node.js +7 -1
  92. package/dist/server/test-setup-node.js.map +1 -1
  93. package/dist/server/test-setup.js +7 -1
  94. package/dist/server/test-setup.js.map +1 -1
  95. package/dist/server/tsconfig.tsbuildinfo +1 -1
  96. package/dist/server/ui/assets/{ReviewDetailPage-CEMxN6RQ.js → ReviewDetailPage-Dy3PfRrg.js} +1 -1
  97. package/dist/server/ui/assets/{ReviewsPage-BzMksnGd.js → ReviewsPage-DUPZ_NtL.js} +1 -1
  98. package/dist/server/ui/assets/index-BzphIngp.css +32 -0
  99. package/dist/server/ui/assets/{index-Csagg3g2.js → index-CIlp3VXc.js} +219 -219
  100. package/dist/server/ui/assets/{useReviewSubscription-B2ejeWE4.js → useReviewSubscription-jEyfNAEJ.js} +1 -1
  101. package/dist/server/ui/index.html +2 -2
  102. package/dist/templates/3-agents-dev.json +111 -64
  103. package/dist/templates/5-agents-dev.json +153 -106
  104. package/dist/templates/teams-dev.json +160 -112
  105. package/package.json +18 -3
  106. package/dist/server/ui/assets/index-ChJ1IUMI.css +0 -32
@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.SessionsService = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
+ const promises_1 = require("fs/promises");
17
18
  const error_types_1 = require("../../../common/errors/error-types");
18
19
  const confirmed_delivery_helper_1 = require("../../terminal/services/confirmed-delivery.helper");
19
20
  const core_1 = require("@nestjs/core");
@@ -38,6 +39,7 @@ const hooks_config_service_1 = require("../../hooks/services/hooks-config.servic
38
39
  const provider_adapter_factory_1 = require("../../providers/adapters/provider-adapter.factory");
39
40
  const teams_service_1 = require("../../teams/services/teams.service");
40
41
  const env_config_1 = require("../../../common/config/env.config");
42
+ const host_helpers_1 = require("../../../common/config/host-helpers");
41
43
  const logger = (0, logger_1.createLogger)('SessionsService');
42
44
  const BOOT_ID = (0, crypto_1.randomUUID)();
43
45
  function isUniqueConstraintError(error) {
@@ -67,6 +69,135 @@ let SessionsService = class SessionsService {
67
69
  this.sqlite = this.db.session?.client ?? this.db;
68
70
  logger.info('SessionsService initialized');
69
71
  }
72
+ async resolveLaunchTarget(params) {
73
+ const { agentId, projectId, epicId } = params;
74
+ const agent = await this.storage.getAgent(agentId);
75
+ const project = await this.storage.getProject(projectId);
76
+ if (agent.projectId !== projectId) {
77
+ throw new error_types_1.ValidationError(`Agent ${agentId} does not belong to project ${projectId}.`, {
78
+ agentId,
79
+ agentProjectId: agent.projectId,
80
+ requestedProjectId: projectId,
81
+ });
82
+ }
83
+ const epic = epicId ? await this.storage.getEpic(epicId) : null;
84
+ const profile = await this.storage.getAgentProfile(agent.profileId);
85
+ let provider;
86
+ let options;
87
+ let configEnv = null;
88
+ if (agent.providerConfigId) {
89
+ const config = await this.storage.getProfileProviderConfig(agent.providerConfigId);
90
+ provider = await this.storage.getProvider(config.providerId);
91
+ options = config.options;
92
+ configEnv = config.env;
93
+ logger.info({ agentId, configId: config.id, providerId: provider.id }, 'Resolved provider via config');
94
+ }
95
+ else {
96
+ const configs = await this.storage.listProfileProviderConfigsByProfile(profile.id);
97
+ if (configs.length > 0) {
98
+ const firstConfig = configs[0];
99
+ provider = await this.storage.getProvider(firstConfig.providerId);
100
+ options = firstConfig.options;
101
+ configEnv = firstConfig.env;
102
+ logger.info({ agentId, profileId: profile.id, configId: firstConfig.id, providerId: provider.id }, 'Resolved provider via first profile config (no providerConfigId set on agent)');
103
+ }
104
+ else {
105
+ throw new error_types_1.ValidationError(`Profile ${profile.id} has no provider configs - cannot launch session`);
106
+ }
107
+ }
108
+ return { agent, project, epic, profile, provider, options, configEnv };
109
+ }
110
+ verifyProviderBinary(provider) {
111
+ if (!provider.binPath) {
112
+ throw new error_types_1.ValidationError(`Provider ${provider.name} is missing a binary path. Set the path before launching sessions.`, {
113
+ code: 'PROVIDER_BINARY_NOT_FOUND',
114
+ providerId: provider.id,
115
+ providerName: provider.name,
116
+ });
117
+ }
118
+ }
119
+ composeLaunchEnv(params) {
120
+ const { sessionId, tmuxSessionName, projectId, agentId, provider, configEnv, optionArgs } = params;
121
+ const providerEnv = provider.env ?? {};
122
+ const mergedBaseEnv = { ...providerEnv, ...(configEnv ?? {}) };
123
+ let envVars = Object.keys(mergedBaseEnv).length > 0 ? mergedBaseEnv : null;
124
+ let processedOptionArgs = optionArgs;
125
+ if (provider.name.toLowerCase() === 'claude') {
126
+ const env = (0, env_config_1.getEnvConfig)();
127
+ const devchainEnv = {
128
+ DEVCHAIN_API_URL: (0, host_helpers_1.getRuntimeInternalBaseUrl)(env),
129
+ DEVCHAIN_PROJECT_ID: projectId,
130
+ DEVCHAIN_AGENT_ID: agentId,
131
+ DEVCHAIN_SESSION_ID: sessionId,
132
+ DEVCHAIN_TMUX_SESSION_NAME: tmuxSessionName,
133
+ };
134
+ if (provider.oneMillionContextEnabled) {
135
+ processedOptionArgs = (0, profile_options_1.rewriteModelTo1m)(processedOptionArgs);
136
+ }
137
+ const modelStr = (0, profile_options_1.extractModelFromArgs)(processedOptionArgs);
138
+ const family = modelStr ? (0, profile_options_1.detectClaudeModelFamily)(modelStr) : null;
139
+ if (provider.oneMillionContextEnabled &&
140
+ family === 'opus' &&
141
+ provider.autoCompactThreshold1m != null) {
142
+ devchainEnv.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(provider.autoCompactThreshold1m);
143
+ }
144
+ else if (provider.autoCompactThreshold != null) {
145
+ devchainEnv.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(provider.autoCompactThreshold);
146
+ }
147
+ envVars = { ...devchainEnv, ...providerEnv, ...(configEnv ?? {}) };
148
+ delete envVars.CLAUDE_CODE_DISABLE_1M_CONTEXT;
149
+ }
150
+ return { envVars, processedOptionArgs };
151
+ }
152
+ async ensureMcpConfig(provider, projectRootPath) {
153
+ let preflightResult = await this.preflightService.runChecks(projectRootPath);
154
+ let providerCheck = preflightResult.providers?.find((p) => p.id === provider.id);
155
+ if (providerCheck?.mcpStatus && providerCheck.mcpStatus !== 'pass') {
156
+ logger.info({
157
+ providerId: provider.id,
158
+ providerName: provider.name,
159
+ mcpStatus: providerCheck.mcpStatus,
160
+ }, 'MCP not configured, attempting auto-ensure');
161
+ const ensureResult = await this.mcpEnsureService.ensureMcp(provider, projectRootPath);
162
+ if (ensureResult.success) {
163
+ logger.info({ providerId: provider.id, action: ensureResult.action }, 'MCP auto-configured successfully');
164
+ if (ensureResult.warnings?.length) {
165
+ for (const w of ensureResult.warnings) {
166
+ logger.warn({ providerId: provider.id, ...w }, 'MCP ensure warning');
167
+ }
168
+ }
169
+ preflightResult = await this.preflightService.runChecks(projectRootPath);
170
+ providerCheck = preflightResult.providers?.find((p) => p.id === provider.id);
171
+ }
172
+ else {
173
+ logger.warn({ providerId: provider.id, message: ensureResult.message }, 'MCP auto-ensure failed');
174
+ }
175
+ if (providerCheck?.mcpStatus && providerCheck.mcpStatus !== 'pass') {
176
+ throw new error_types_1.ValidationError('Provider MCP is not configured', {
177
+ code: 'MCP_NOT_CONFIGURED',
178
+ providerId: provider.id,
179
+ providerName: provider.name,
180
+ mcpStatus: providerCheck.mcpStatus,
181
+ mcpMessage: providerCheck.mcpMessage,
182
+ });
183
+ }
184
+ }
185
+ else if (provider.name.toLowerCase() === 'gemini' && projectRootPath) {
186
+ await this.mcpEnsureService.ensureMcp(provider, projectRootPath);
187
+ }
188
+ return preflightResult;
189
+ }
190
+ async setupHooksConfig(provider, projectRootPath) {
191
+ if (provider.name.toLowerCase() !== 'claude')
192
+ return;
193
+ try {
194
+ await this.hooksConfigService.ensureHooksConfig(projectRootPath);
195
+ logger.info({ projectRootPath }, 'Hooks config ensured for Claude provider');
196
+ }
197
+ catch (error) {
198
+ logger.warn({ error }, 'Failed to ensure hooks config (non-fatal)');
199
+ }
200
+ }
70
201
  async launchSession(data) {
71
202
  const { epicId, agentId, projectId, options: launchOptions } = data;
72
203
  const silent = launchOptions?.silent === true;
@@ -119,40 +250,7 @@ let SessionsService = class SessionsService {
119
250
  .run(new Date().toISOString(), new Date().toISOString(), existingSession.id);
120
251
  logger.info({ agentId }, 'Orphaned session cleaned up, proceeding to create new session');
121
252
  }
122
- const agent = await this.storage.getAgent(agentId);
123
- const project = await this.storage.getProject(projectId);
124
- if (agent.projectId !== projectId) {
125
- throw new error_types_1.ValidationError(`Agent ${agentId} does not belong to project ${projectId}.`, {
126
- agentId,
127
- agentProjectId: agent.projectId,
128
- requestedProjectId: projectId,
129
- });
130
- }
131
- const epic = epicId ? await this.storage.getEpic(epicId) : null;
132
- const profile = await this.storage.getAgentProfile(agent.profileId);
133
- let provider;
134
- let options;
135
- let configEnv = null;
136
- if (agent.providerConfigId) {
137
- const config = await this.storage.getProfileProviderConfig(agent.providerConfigId);
138
- provider = await this.storage.getProvider(config.providerId);
139
- options = config.options;
140
- configEnv = config.env;
141
- logger.info({ agentId, configId: config.id, providerId: provider.id }, 'Resolved provider via config');
142
- }
143
- else {
144
- const configs = await this.storage.listProfileProviderConfigsByProfile(profile.id);
145
- if (configs.length > 0) {
146
- const firstConfig = configs[0];
147
- provider = await this.storage.getProvider(firstConfig.providerId);
148
- options = firstConfig.options;
149
- configEnv = firstConfig.env;
150
- logger.info({ agentId, profileId: profile.id, configId: firstConfig.id, providerId: provider.id }, 'Resolved provider via first profile config (no providerConfigId set on agent)');
151
- }
152
- else {
153
- throw new error_types_1.ValidationError(`Profile ${profile.id} has no provider configs - cannot launch session`);
154
- }
155
- }
253
+ const { agent, project, epic, profile, provider, options, configEnv } = await this.resolveLaunchTarget({ agentId, projectId, epicId });
156
254
  if (provider.name.toLowerCase() === 'claude') {
157
255
  const { autoCompactEnabled, configState } = await (0, claude_config_1.checkClaudeAutoCompact)();
158
256
  if (!autoCompactEnabled && configState !== 'malformed') {
@@ -167,42 +265,8 @@ let SessionsService = class SessionsService {
167
265
  });
168
266
  }
169
267
  }
170
- let preflightResult = await this.preflightService.runChecks(project.rootPath);
171
- let providerCheck = preflightResult.providers?.find((p) => p.id === provider.id);
172
- if (providerCheck?.mcpStatus && providerCheck.mcpStatus !== 'pass') {
173
- logger.info({
174
- providerId: provider.id,
175
- providerName: provider.name,
176
- mcpStatus: providerCheck.mcpStatus,
177
- }, 'MCP not configured, attempting auto-ensure');
178
- const ensureResult = await this.mcpEnsureService.ensureMcp(provider, project.rootPath);
179
- if (ensureResult.success) {
180
- logger.info({ providerId: provider.id, action: ensureResult.action }, 'MCP auto-configured successfully');
181
- preflightResult = await this.preflightService.runChecks(project.rootPath);
182
- providerCheck = preflightResult.providers?.find((p) => p.id === provider.id);
183
- }
184
- else {
185
- logger.warn({ providerId: provider.id, message: ensureResult.message }, 'MCP auto-ensure failed');
186
- }
187
- if (providerCheck?.mcpStatus && providerCheck.mcpStatus !== 'pass') {
188
- throw new error_types_1.ValidationError('Provider MCP is not configured', {
189
- code: 'MCP_NOT_CONFIGURED',
190
- providerId: provider.id,
191
- providerName: provider.name,
192
- mcpStatus: providerCheck.mcpStatus,
193
- mcpMessage: providerCheck.mcpMessage,
194
- });
195
- }
196
- }
197
- if (provider.name.toLowerCase() === 'claude') {
198
- try {
199
- await this.hooksConfigService.ensureHooksConfig(project.rootPath);
200
- logger.info({ projectId, projectRootPath: project.rootPath }, 'Hooks config ensured for Claude provider');
201
- }
202
- catch (error) {
203
- logger.warn({ error, projectId }, 'Failed to ensure hooks config (non-fatal)');
204
- }
205
- }
268
+ const preflightResult = await this.ensureMcpConfig(provider, project.rootPath);
269
+ await this.setupHooksConfig(provider, project.rootPath);
206
270
  if (preflightResult.overall === 'fail') {
207
271
  const failedChecks = preflightResult.checks
208
272
  .filter((c) => c.status === 'fail')
@@ -224,12 +288,7 @@ let SessionsService = class SessionsService {
224
288
  await this.tmuxService.createSession(tmuxSessionName, project.rootPath);
225
289
  await this.tmuxService.setAlternateScreenOff(tmuxSessionName);
226
290
  this.tmuxService.startHealthCheck(tmuxSessionName, sessionId);
227
- if (!provider.binPath) {
228
- throw new error_types_1.ValidationError(`Provider ${provider.name} is missing a binary path. Set the path before launching sessions.`, {
229
- providerId: provider.id,
230
- providerName: provider.name,
231
- });
232
- }
291
+ this.verifyProviderBinary(provider);
233
292
  let optionArgs = [];
234
293
  try {
235
294
  optionArgs = (0, profile_options_1.parseProfileOptions)(options);
@@ -246,37 +305,29 @@ let SessionsService = class SessionsService {
246
305
  if (agent.modelOverride) {
247
306
  optionArgs = (0, profile_options_1.injectModelOverride)(optionArgs, agent.modelOverride);
248
307
  }
249
- const providerEnv = provider.env ?? {};
250
- const mergedBaseEnv = { ...providerEnv, ...(configEnv ?? {}) };
251
- let envVars = Object.keys(mergedBaseEnv).length > 0 ? mergedBaseEnv : null;
252
- if (provider.name.toLowerCase() === 'claude') {
253
- const env = (0, env_config_1.getEnvConfig)();
254
- const devchainEnv = {
255
- DEVCHAIN_API_URL: `http://127.0.0.1:${env.PORT}`,
256
- DEVCHAIN_PROJECT_ID: projectId,
257
- DEVCHAIN_AGENT_ID: agentId,
258
- DEVCHAIN_SESSION_ID: sessionId,
259
- DEVCHAIN_TMUX_SESSION_NAME: tmuxSessionName,
260
- };
261
- if (provider.oneMillionContextEnabled) {
262
- optionArgs = (0, profile_options_1.rewriteModelTo1m)(optionArgs);
263
- }
264
- const modelStr = (0, profile_options_1.extractModelFromArgs)(optionArgs);
265
- const family = modelStr ? (0, profile_options_1.detectClaudeModelFamily)(modelStr) : null;
266
- if (provider.oneMillionContextEnabled &&
267
- family === 'opus' &&
268
- provider.autoCompactThreshold1m != null) {
269
- devchainEnv.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(provider.autoCompactThreshold1m);
270
- }
271
- else if (provider.autoCompactThreshold != null) {
272
- devchainEnv.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = String(provider.autoCompactThreshold);
273
- }
274
- envVars = { ...devchainEnv, ...providerEnv, ...(configEnv ?? {}) };
275
- delete envVars.CLAUDE_CODE_DISABLE_1M_CONTEXT;
308
+ const { envVars, processedOptionArgs } = this.composeLaunchEnv({
309
+ sessionId,
310
+ tmuxSessionName,
311
+ projectId,
312
+ agentId,
313
+ provider,
314
+ configEnv,
315
+ optionArgs,
316
+ });
317
+ optionArgs = processedOptionArgs;
318
+ let launchArgv = optionArgs;
319
+ try {
320
+ const launchAdapter = this.providerAdapterFactory.getAdapter(provider.name);
321
+ launchArgv = launchAdapter.buildLaunchArgs({
322
+ mode: 'new',
323
+ profileOptionArgs: optionArgs,
324
+ }).argv;
325
+ }
326
+ catch {
276
327
  }
277
328
  let commandArgs;
278
329
  try {
279
- commandArgs = (0, env_builder_1.buildSessionCommand)(envVars, provider.binPath, optionArgs);
330
+ commandArgs = (0, env_builder_1.buildSessionCommand)(envVars, provider.binPath, launchArgv);
280
331
  }
281
332
  catch (error) {
282
333
  if (error instanceof env_builder_1.EnvBuilderError) {
@@ -290,10 +341,10 @@ let SessionsService = class SessionsService {
290
341
  try {
291
342
  this.sqlite
292
343
  .prepare(`
293
- INSERT INTO sessions (id, epic_id, agent_id, tmux_session_id, status, started_at, created_at, updated_at)
294
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
344
+ INSERT INTO sessions (id, epic_id, agent_id, tmux_session_id, status, started_at, provider_name_at_launch, created_at, updated_at)
345
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
295
346
  `)
296
- .run(sessionId, epicId ?? null, agentId, tmuxSessionName, 'running', now, now, now);
347
+ .run(sessionId, epicId ?? null, agentId, tmuxSessionName, 'running', now, provider.name.toLowerCase(), now, now);
297
348
  logger.info({ sessionId, tmuxSessionName }, 'Session created in database');
298
349
  }
299
350
  catch (error) {
@@ -427,7 +478,6 @@ let SessionsService = class SessionsService {
427
478
  startedAt: now,
428
479
  endedAt: null,
429
480
  transcriptPath: null,
430
- claudeSessionId: null,
431
481
  createdAt: now,
432
482
  updatedAt: now,
433
483
  epic: epic
@@ -450,6 +500,236 @@ let SessionsService = class SessionsService {
450
500
  };
451
501
  });
452
502
  }
503
+ async restoreSession(sessionId, projectId) {
504
+ const source = this.sqlite
505
+ .prepare(`SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at,
506
+ transcript_path, provider_session_id, provider_name_at_launch, created_at, updated_at
507
+ FROM sessions WHERE id = ?`)
508
+ .get(sessionId);
509
+ if (!source) {
510
+ throw new common_1.NotFoundException('Session not found');
511
+ }
512
+ const sourceAgent = await this.storage.getAgent(source.agent_id);
513
+ if (sourceAgent.projectId !== projectId) {
514
+ throw new common_1.ForbiddenException('PROJECT_MISMATCH');
515
+ }
516
+ if (source.status !== 'stopped' && source.status !== 'failed') {
517
+ throw new common_1.ConflictException({
518
+ message: 'Session is not in a restorable state',
519
+ code: 'INVALID_SESSION_STATE',
520
+ });
521
+ }
522
+ if (!source.provider_session_id) {
523
+ throw new common_1.ConflictException({
524
+ message: 'Session has no provider session ID',
525
+ code: 'NO_PROVIDER_SESSION_ID',
526
+ });
527
+ }
528
+ const { provider: currentProvider } = await this.resolveLaunchTarget({
529
+ agentId: source.agent_id,
530
+ projectId,
531
+ epicId: source.epic_id,
532
+ });
533
+ if (source.provider_name_at_launch &&
534
+ currentProvider.name.toLowerCase() !== source.provider_name_at_launch.toLowerCase()) {
535
+ throw new common_1.ConflictException({
536
+ message: 'Current provider differs from launch-time provider',
537
+ code: 'PROVIDER_MISMATCH',
538
+ });
539
+ }
540
+ return this.sessionCoordinator.withAgentLock(source.agent_id, async () => {
541
+ const lockedSource = this.sqlite
542
+ .prepare(`SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at,
543
+ transcript_path, provider_session_id, provider_name_at_launch, created_at, updated_at
544
+ FROM sessions WHERE id = ?`)
545
+ .get(sessionId);
546
+ if (!lockedSource) {
547
+ throw new common_1.NotFoundException('Session not found');
548
+ }
549
+ if (lockedSource.status !== 'stopped' && lockedSource.status !== 'failed') {
550
+ throw new common_1.ConflictException({
551
+ message: 'Session is not in a restorable state',
552
+ code: 'INVALID_SESSION_STATE',
553
+ });
554
+ }
555
+ if (!lockedSource.provider_session_id) {
556
+ throw new common_1.ConflictException({
557
+ message: 'Session has no provider session ID',
558
+ code: 'NO_PROVIDER_SESSION_ID',
559
+ });
560
+ }
561
+ if (lockedSource.provider_name_at_launch &&
562
+ currentProvider.name.toLowerCase() !== lockedSource.provider_name_at_launch.toLowerCase()) {
563
+ throw new common_1.ConflictException({
564
+ message: 'Current provider differs from launch-time provider',
565
+ code: 'PROVIDER_MISMATCH',
566
+ });
567
+ }
568
+ const existingRunning = this.getActiveSessionForAgent(lockedSource.agent_id);
569
+ if (existingRunning) {
570
+ throw new common_1.ConflictException({
571
+ message: 'Agent already has a running session',
572
+ code: 'INVALID_SESSION_STATE',
573
+ });
574
+ }
575
+ const prior = {
576
+ status: lockedSource.status,
577
+ ended_at: lockedSource.ended_at,
578
+ tmux_session_id: lockedSource.tmux_session_id,
579
+ };
580
+ const { agent, project, epic, profile, provider, options, configEnv } = await this.resolveLaunchTarget({
581
+ agentId: lockedSource.agent_id,
582
+ projectId,
583
+ epicId: lockedSource.epic_id,
584
+ });
585
+ this.verifyProviderBinary(provider);
586
+ if (lockedSource.provider_name_at_launch &&
587
+ provider.name.toLowerCase() !== lockedSource.provider_name_at_launch.toLowerCase()) {
588
+ throw new common_1.ConflictException({
589
+ message: 'Current provider differs from launch-time provider',
590
+ code: 'PROVIDER_MISMATCH',
591
+ });
592
+ }
593
+ let optionArgs = [];
594
+ try {
595
+ optionArgs = (0, profile_options_1.parseProfileOptions)(options);
596
+ }
597
+ catch (error) {
598
+ if (error instanceof profile_options_1.ProfileOptionsError) {
599
+ throw new error_types_1.ValidationError(error.message, {
600
+ profileId: profile.id,
601
+ profileName: profile.name,
602
+ });
603
+ }
604
+ throw error;
605
+ }
606
+ if (agent.modelOverride) {
607
+ optionArgs = (0, profile_options_1.injectModelOverride)(optionArgs, agent.modelOverride);
608
+ }
609
+ const projectSlug = project.name
610
+ .toLowerCase()
611
+ .replace(/[^a-z0-9]+/g, '-')
612
+ .replace(/(^-|-$)/g, '');
613
+ const epicSegment = lockedSource.epic_id ?? 'independent';
614
+ const tmuxSessionName = this.tmuxService.createSessionName(projectSlug, epicSegment, agent.id, lockedSource.id);
615
+ const { envVars, processedOptionArgs } = this.composeLaunchEnv({
616
+ sessionId: lockedSource.id,
617
+ tmuxSessionName,
618
+ projectId,
619
+ agentId: agent.id,
620
+ provider,
621
+ configEnv,
622
+ optionArgs,
623
+ });
624
+ optionArgs = processedOptionArgs;
625
+ const adapter = this.providerAdapterFactory.getAdapter(provider.name);
626
+ const launchArgv = adapter.buildLaunchArgs({
627
+ mode: 'restore',
628
+ providerSessionId: lockedSource.provider_session_id,
629
+ profileOptionArgs: optionArgs,
630
+ }).argv;
631
+ if (!launchArgv.includes(lockedSource.provider_session_id)) {
632
+ throw new error_types_1.ValidationError('Restore argv does not include provider session ID — adapter contract violation', {
633
+ code: 'RESTORE_ARGS_UNAVAILABLE',
634
+ providerName: provider.name,
635
+ providerSessionId: lockedSource.provider_session_id,
636
+ });
637
+ }
638
+ let commandArgs;
639
+ try {
640
+ commandArgs = (0, env_builder_1.buildSessionCommand)(envVars, provider.binPath, launchArgv);
641
+ }
642
+ catch (error) {
643
+ if (error instanceof env_builder_1.EnvBuilderError) {
644
+ throw new error_types_1.ValidationError(error.message, {
645
+ agentId: agent.id,
646
+ providerConfigId: agent.providerConfigId,
647
+ });
648
+ }
649
+ throw error;
650
+ }
651
+ const now = new Date().toISOString();
652
+ this.sqlite
653
+ .prepare(`UPDATE sessions
654
+ SET status = 'running', tmux_session_id = ?, ended_at = NULL,
655
+ last_activity_at = ?, updated_at = ?
656
+ WHERE id = ?`)
657
+ .run(tmuxSessionName, now, now, lockedSource.id);
658
+ try {
659
+ await this.tmuxService.createSession(tmuxSessionName, project.rootPath);
660
+ }
661
+ catch (tmuxError) {
662
+ this.sqlite
663
+ .prepare(`UPDATE sessions
664
+ SET status = ?, ended_at = ?, tmux_session_id = ?, updated_at = ?
665
+ WHERE id = ?`)
666
+ .run(prior.status, prior.ended_at, prior.tmux_session_id, now, lockedSource.id);
667
+ logger.error({ sessionId: lockedSource.id, error: String(tmuxError) }, 'Restore failed: tmux creation error — rolled back');
668
+ throw new common_1.InternalServerErrorException('RESTORE_FAILED');
669
+ }
670
+ await this.tmuxService.setAlternateScreenOff(tmuxSessionName);
671
+ this.tmuxService.startHealthCheck(tmuxSessionName, lockedSource.id);
672
+ try {
673
+ await this.tmuxService.sendCommandArgs(tmuxSessionName, commandArgs);
674
+ }
675
+ catch (sendError) {
676
+ this.sqlite
677
+ .prepare(`UPDATE sessions
678
+ SET status = ?, ended_at = ?, tmux_session_id = ?, updated_at = ?
679
+ WHERE id = ?`)
680
+ .run(prior.status, prior.ended_at, prior.tmux_session_id, now, lockedSource.id);
681
+ try {
682
+ await this.tmuxService.destroySession(tmuxSessionName);
683
+ }
684
+ catch (destroyErr) {
685
+ logger.warn({ tmuxSessionName, error: String(destroyErr) }, 'Failed to destroy tmux session during restore rollback');
686
+ }
687
+ logger.error({ sessionId: lockedSource.id, error: String(sendError) }, 'Restore failed: CLI launch error — rolled back');
688
+ throw new common_1.InternalServerErrorException('RESTORE_FAILED');
689
+ }
690
+ await this.ptyService.startStreaming(lockedSource.id, tmuxSessionName);
691
+ await this.getEventsService().publish('session.restored', {
692
+ sessionId: lockedSource.id,
693
+ epicId: lockedSource.epic_id,
694
+ agentId: agent.id,
695
+ tmuxSessionName,
696
+ });
697
+ if (lockedSource.transcript_path) {
698
+ await this.getEventsService().publish('session.transcript.discovered', {
699
+ sessionId: lockedSource.id,
700
+ agentId: agent.id,
701
+ projectId,
702
+ transcriptPath: lockedSource.transcript_path,
703
+ providerName: provider.name.toLowerCase(),
704
+ });
705
+ }
706
+ try {
707
+ this.getTerminalGateway().broadcastEvent(`agent/${agent.id}`, 'presence', {
708
+ online: true,
709
+ sessionId: lockedSource.id,
710
+ agentId: agent.id,
711
+ });
712
+ }
713
+ catch (error) {
714
+ logger.warn({ error, agentId: agent.id }, 'Failed to broadcast presence after restore');
715
+ }
716
+ return {
717
+ id: lockedSource.id,
718
+ epicId: lockedSource.epic_id,
719
+ agentId: agent.id,
720
+ tmuxSessionId: tmuxSessionName,
721
+ status: 'running',
722
+ startedAt: lockedSource.started_at,
723
+ endedAt: null,
724
+ transcriptPath: lockedSource.transcript_path,
725
+ createdAt: lockedSource.created_at,
726
+ updatedAt: now,
727
+ epic: epic ? { id: epic.id, title: epic.title, projectId: epic.projectId } : null,
728
+ agent: { id: agent.id, name: agent.name, profileId: agent.profileId },
729
+ project: { id: project.id, name: project.name, rootPath: project.rootPath },
730
+ };
731
+ });
732
+ }
453
733
  async terminateSession(sessionId) {
454
734
  logger.info({ sessionId }, 'Terminating session');
455
735
  const session = this.getSession(sessionId);
@@ -471,14 +751,24 @@ let SessionsService = class SessionsService {
471
751
  logger.warn({ sessionId, tmuxSessionId: session.tmuxSessionId }, 'Tmux session already gone, cleaning up database record');
472
752
  }
473
753
  }
754
+ let sizeBytes = null;
755
+ if (session.transcriptPath) {
756
+ try {
757
+ const fileStat = await (0, promises_1.stat)(session.transcriptPath);
758
+ sizeBytes = fileStat.size;
759
+ }
760
+ catch (error) {
761
+ logger.warn({ error, sessionId, transcriptPath: session.transcriptPath }, 'Could not stat transcript file for size_bytes — leaving NULL (best-effort)');
762
+ }
763
+ }
474
764
  const now = new Date().toISOString();
475
765
  this.sqlite
476
766
  .prepare(`
477
767
  UPDATE sessions
478
- SET status = ?, ended_at = ?, updated_at = ?
768
+ SET status = ?, ended_at = ?, size_bytes = ?, updated_at = ?
479
769
  WHERE id = ?
480
770
  `)
481
- .run('stopped', now, now, sessionId);
771
+ .run('stopped', now, sizeBytes, now, sessionId);
482
772
  logger.info({ sessionId }, 'Session terminated');
483
773
  await this.getEventsService().publish('session.stopped', { sessionId });
484
774
  if (session.agentId) {
@@ -494,10 +784,86 @@ let SessionsService = class SessionsService {
494
784
  }
495
785
  }
496
786
  }
787
+ async getAgentSessionHistory(agentId, projectId, cursor, limit) {
788
+ const agent = await this.storage.getAgent(agentId);
789
+ if (agent.projectId !== projectId) {
790
+ throw new common_1.ForbiddenException('PROJECT_MISMATCH');
791
+ }
792
+ const clampedLimit = Math.min(Math.max(1, limit), 100);
793
+ const sortExpr = `COALESCE(last_activity_at, ended_at, started_at)`;
794
+ const selectCols = `id, provider_session_id, provider_name_at_launch, status, started_at, ended_at, last_activity_at, size_bytes, transcript_path`;
795
+ const baseStatus = `status IN ('stopped','failed')`;
796
+ const { cnt: total } = this.sqlite
797
+ .prepare(`SELECT COUNT(*) as cnt FROM sessions WHERE agent_id = ? AND ${baseStatus}`)
798
+ .get(agentId);
799
+ let cursorSortKey = null;
800
+ let cursorId = null;
801
+ if (cursor) {
802
+ try {
803
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
804
+ cursorSortKey = decoded.s;
805
+ cursorId = decoded.i;
806
+ }
807
+ catch {
808
+ }
809
+ }
810
+ let rows;
811
+ if (cursorSortKey && cursorId) {
812
+ rows = this.sqlite
813
+ .prepare(`SELECT ${selectCols} FROM sessions
814
+ WHERE agent_id = ? AND ${baseStatus}
815
+ AND (${sortExpr} < ? OR (${sortExpr} = ? AND id < ?))
816
+ ORDER BY ${sortExpr} DESC, id DESC
817
+ LIMIT ?`)
818
+ .all(agentId, cursorSortKey, cursorSortKey, cursorId, clampedLimit + 1);
819
+ }
820
+ else {
821
+ rows = this.sqlite
822
+ .prepare(`SELECT ${selectCols} FROM sessions
823
+ WHERE agent_id = ? AND ${baseStatus}
824
+ ORDER BY ${sortExpr} DESC, id DESC
825
+ LIMIT ?`)
826
+ .all(agentId, clampedLimit + 1);
827
+ }
828
+ const hasMore = rows.length > clampedLimit;
829
+ const pageRows = hasMore ? rows.slice(0, clampedLimit) : rows;
830
+ const now = new Date().toISOString();
831
+ for (const row of pageRows) {
832
+ if (row.size_bytes === null && row.transcript_path !== null) {
833
+ try {
834
+ const fileStat = await (0, promises_1.stat)(row.transcript_path);
835
+ this.sqlite
836
+ .prepare(`UPDATE sessions SET size_bytes = ?, updated_at = ? WHERE id = ?`)
837
+ .run(fileStat.size, now, row.id);
838
+ row.size_bytes = fileStat.size;
839
+ }
840
+ catch {
841
+ }
842
+ }
843
+ }
844
+ let nextCursor = null;
845
+ if (hasMore && pageRows.length > 0) {
846
+ const last = pageRows[pageRows.length - 1];
847
+ const sortKey = last.last_activity_at ?? last.ended_at ?? last.started_at;
848
+ nextCursor = Buffer.from(JSON.stringify({ s: sortKey, i: last.id })).toString('base64url');
849
+ }
850
+ const items = pageRows.map((row) => ({
851
+ id: row.id,
852
+ providerSessionId: row.provider_session_id ?? null,
853
+ providerNameAtLaunch: row.provider_name_at_launch,
854
+ status: row.status,
855
+ startedAt: row.started_at,
856
+ endedAt: row.ended_at,
857
+ lastActivityAt: row.last_activity_at,
858
+ sizeBytes: row.size_bytes,
859
+ transcriptAvailable: row.transcript_path !== null,
860
+ }));
861
+ return { items, nextCursor, hasMore, total };
862
+ }
497
863
  getSession(sessionId) {
498
864
  const row = this.sqlite
499
865
  .prepare(`
500
- SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at, last_activity_at, activity_state, busy_since, transcript_path, claude_session_id, created_at, updated_at
866
+ SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at, last_activity_at, activity_state, busy_since, transcript_path, created_at, updated_at
501
867
  FROM sessions
502
868
  WHERE id = ?
503
869
  `)
@@ -518,7 +884,6 @@ let SessionsService = class SessionsService {
518
884
  activityState: row.activity_state ?? null,
519
885
  busySince: row.busy_since ?? null,
520
886
  transcriptPath: row.transcript_path ?? null,
521
- claudeSessionId: row.claude_session_id ?? null,
522
887
  createdAt: row.created_at,
523
888
  updatedAt: row.updated_at,
524
889
  };
@@ -526,7 +891,7 @@ let SessionsService = class SessionsService {
526
891
  async listActiveSessions(projectId, allowedAgentIds) {
527
892
  const rows = this.sqlite
528
893
  .prepare(`
529
- SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at, last_activity_at, activity_state, busy_since, transcript_path, claude_session_id, created_at, updated_at
894
+ SELECT id, epic_id, agent_id, tmux_session_id, status, started_at, ended_at, last_activity_at, activity_state, busy_since, transcript_path, created_at, updated_at
530
895
  FROM sessions
531
896
  WHERE status = 'running'
532
897
  ORDER BY started_at DESC
@@ -564,7 +929,6 @@ let SessionsService = class SessionsService {
564
929
  activityState: row.activity_state ?? null,
565
930
  busySince: row.busy_since ?? null,
566
931
  transcriptPath: row.transcript_path ?? null,
567
- claudeSessionId: row.claude_session_id ?? null,
568
932
  createdAt: row.created_at,
569
933
  updatedAt: row.updated_at,
570
934
  }));
@@ -583,7 +947,7 @@ let SessionsService = class SessionsService {
583
947
  .prepare(`
584
948
  SELECT id, epic_id, agent_id, tmux_session_id, status,
585
949
  started_at, ended_at, last_activity_at, activity_state,
586
- busy_since, transcript_path, claude_session_id,
950
+ busy_since, transcript_path,
587
951
  created_at, updated_at
588
952
  FROM sessions
589
953
  WHERE status = 'running' AND agent_id = ?
@@ -606,7 +970,6 @@ let SessionsService = class SessionsService {
606
970
  activityState: row.activity_state ?? null,
607
971
  busySince: row.busy_since ?? null,
608
972
  transcriptPath: row.transcript_path ?? null,
609
- claudeSessionId: row.claude_session_id ?? null,
610
973
  createdAt: row.created_at,
611
974
  updatedAt: row.updated_at,
612
975
  };
@@ -616,7 +979,7 @@ let SessionsService = class SessionsService {
616
979
  .prepare(`
617
980
  SELECT s.id, s.epic_id, s.agent_id, s.tmux_session_id, s.status,
618
981
  s.started_at, s.ended_at, s.last_activity_at, s.activity_state,
619
- s.busy_since, s.transcript_path, s.claude_session_id,
982
+ s.busy_since, s.transcript_path,
620
983
  s.created_at, s.updated_at
621
984
  FROM sessions s
622
985
  JOIN agents a ON s.agent_id = a.id
@@ -636,7 +999,6 @@ let SessionsService = class SessionsService {
636
999
  activityState: row.activity_state ?? null,
637
1000
  busySince: row.busy_since ?? null,
638
1001
  transcriptPath: row.transcript_path ?? null,
639
- claudeSessionId: row.claude_session_id ?? null,
640
1002
  createdAt: row.created_at,
641
1003
  updatedAt: row.updated_at,
642
1004
  }));