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.
- package/README.md +4 -0
- package/dist/cli.js +154 -33
- package/dist/drizzle/0060_huge_proemial_gods.sql +12 -0
- package/dist/drizzle/0061_easy_silver_surfer.sql +2 -0
- package/dist/drizzle/meta/0060_snapshot.json +5170 -0
- package/dist/drizzle/meta/0061_snapshot.json +5163 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/server/common/config/env.config.d.ts +1 -1
- package/dist/server/common/config/env.config.js +9 -1
- package/dist/server/common/config/env.config.js.map +1 -1
- package/dist/server/common/config/host-helpers.d.ts +7 -0
- package/dist/server/common/config/host-helpers.js +34 -0
- package/dist/server/common/config/host-helpers.js.map +1 -0
- package/dist/server/main.js +5 -3
- package/dist/server/main.js.map +1 -1
- package/dist/server/modules/core/core-normal.module.js +3 -2
- package/dist/server/modules/core/core-normal.module.js.map +1 -1
- package/dist/server/modules/core/services/gemini-trusted-folders.service.d.ts +26 -0
- package/dist/server/modules/core/services/gemini-trusted-folders.service.js +181 -0
- package/dist/server/modules/core/services/gemini-trusted-folders.service.js.map +1 -0
- package/dist/server/modules/core/services/preflight.service.js +2 -1
- package/dist/server/modules/core/services/preflight.service.js.map +1 -1
- package/dist/server/modules/core/services/provider-mcp-ensure.service.d.ts +10 -1
- package/dist/server/modules/core/services/provider-mcp-ensure.service.js +86 -19
- package/dist/server/modules/core/services/provider-mcp-ensure.service.js.map +1 -1
- package/dist/server/modules/events/catalog/claude.hooks.session.started.d.ts +2 -2
- package/dist/server/modules/events/catalog/index.d.ts +20 -2
- package/dist/server/modules/events/catalog/index.js +5 -1
- package/dist/server/modules/events/catalog/index.js.map +1 -1
- package/dist/server/modules/events/catalog/session.restored.d.ts +21 -0
- package/dist/server/modules/events/catalog/session.restored.js +14 -0
- package/dist/server/modules/events/catalog/session.restored.js.map +1 -0
- package/dist/server/modules/hooks/dtos/hook-event.dto.d.ts +2 -2
- package/dist/server/modules/mcp/services/mcp-provider-registration.service.d.ts +1 -0
- package/dist/server/modules/mcp/services/mcp-provider-registration.service.js +91 -4
- package/dist/server/modules/mcp/services/mcp-provider-registration.service.js.map +1 -1
- package/dist/server/modules/projects/controllers/projects.controller.d.ts +4 -1
- package/dist/server/modules/projects/controllers/projects.controller.js +1 -1
- package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
- package/dist/server/modules/projects/projects.module.js +11 -2
- package/dist/server/modules/projects/projects.module.js.map +1 -1
- package/dist/server/modules/projects/services/project-provider-provisioning.service.d.ts +18 -0
- package/dist/server/modules/projects/services/project-provider-provisioning.service.js +92 -0
- package/dist/server/modules/projects/services/project-provider-provisioning.service.js.map +1 -0
- package/dist/server/modules/projects/services/projects.service.d.ts +9 -2
- package/dist/server/modules/projects/services/projects.service.js +31 -4
- package/dist/server/modules/projects/services/projects.service.js.map +1 -1
- package/dist/server/modules/providers/adapters/claude.adapter.d.ts +4 -1
- package/dist/server/modules/providers/adapters/claude.adapter.js +6 -0
- package/dist/server/modules/providers/adapters/claude.adapter.js.map +1 -1
- package/dist/server/modules/providers/adapters/codex.adapter.d.ts +4 -1
- package/dist/server/modules/providers/adapters/codex.adapter.js +6 -0
- package/dist/server/modules/providers/adapters/codex.adapter.js.map +1 -1
- package/dist/server/modules/providers/adapters/gemini.adapter.d.ts +6 -1
- package/dist/server/modules/providers/adapters/gemini.adapter.js +14 -3
- package/dist/server/modules/providers/adapters/gemini.adapter.js.map +1 -1
- package/dist/server/modules/providers/adapters/opencode.adapter.d.ts +4 -1
- package/dist/server/modules/providers/adapters/opencode.adapter.js +6 -0
- package/dist/server/modules/providers/adapters/opencode.adapter.js.map +1 -1
- package/dist/server/modules/providers/adapters/provider-adapter.interface.d.ts +10 -0
- package/dist/server/modules/providers/controllers/providers.controller.d.ts +1 -0
- package/dist/server/modules/providers/controllers/providers.controller.js +1 -0
- package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
- package/dist/server/modules/reviews/dtos/review.dto.d.ts +2 -2
- package/dist/server/modules/session-reader/data/pricing.json +20 -0
- package/dist/server/modules/session-reader/services/transcript-persistence.listener.js +3 -3
- package/dist/server/modules/session-reader/services/transcript-persistence.listener.js.map +1 -1
- package/dist/server/modules/sessions/controllers/sessions.controller.d.ts +3 -1
- package/dist/server/modules/sessions/controllers/sessions.controller.js +57 -0
- package/dist/server/modules/sessions/controllers/sessions.controller.js.map +1 -1
- package/dist/server/modules/sessions/dtos/sessions.dto.d.ts +25 -1
- package/dist/server/modules/sessions/dtos/sessions.dto.js +4 -1
- package/dist/server/modules/sessions/dtos/sessions.dto.js.map +1 -1
- package/dist/server/modules/sessions/services/sessions.service.d.ts +35 -1
- package/dist/server/modules/sessions/services/sessions.service.js +480 -118
- package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
- package/dist/server/modules/sessions/utils/env-builder.d.ts +1 -1
- package/dist/server/modules/sessions/utils/env-builder.js +3 -3
- package/dist/server/modules/sessions/utils/env-builder.js.map +1 -1
- package/dist/server/modules/storage/db/schema.d.ts +38 -2
- package/dist/server/modules/storage/db/schema.js +4 -1
- package/dist/server/modules/storage/db/schema.js.map +1 -1
- package/dist/server/modules/subscribers/events/event-fields-catalog.js +12 -0
- package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
- package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +6 -0
- package/dist/server/modules/terminal/gateways/terminal.gateway.js +17 -0
- package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
- package/dist/server/templates/3-agents-dev.json +111 -64
- package/dist/server/templates/5-agents-dev.json +153 -106
- package/dist/server/templates/teams-dev.json +160 -112
- package/dist/server/test-setup-node.js +7 -1
- package/dist/server/test-setup-node.js.map +1 -1
- package/dist/server/test-setup.js +7 -1
- package/dist/server/test-setup.js.map +1 -1
- package/dist/server/tsconfig.tsbuildinfo +1 -1
- package/dist/server/ui/assets/{ReviewDetailPage-CEMxN6RQ.js → ReviewDetailPage-Dy3PfRrg.js} +1 -1
- package/dist/server/ui/assets/{ReviewsPage-BzMksnGd.js → ReviewsPage-DUPZ_NtL.js} +1 -1
- package/dist/server/ui/assets/index-BzphIngp.css +32 -0
- package/dist/server/ui/assets/{index-Csagg3g2.js → index-CIlp3VXc.js} +219 -219
- package/dist/server/ui/assets/{useReviewSubscription-B2ejeWE4.js → useReviewSubscription-jEyfNAEJ.js} +1 -1
- package/dist/server/ui/index.html +2 -2
- package/dist/templates/3-agents-dev.json +111 -64
- package/dist/templates/5-agents-dev.json +153 -106
- package/dist/templates/teams-dev.json +160 -112
- package/package.json +18 -3
- 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.
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
}));
|