circuschief 0.1.4 → 0.2.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.
- package/package.json +1 -1
- package/packages/server/src/api/commands.js +50 -55
- package/packages/server/src/api/projects-helpers.js +13 -4
- package/packages/server/src/api/projects.js +33 -18
- package/packages/server/src/cli.js +82 -0
- package/packages/server/src/db/AgentCallLogRepository.js +30 -31
- package/packages/server/src/db/ConversationRepository.js +27 -16
- package/packages/server/src/db/ProjectRepository.js +21 -31
- package/packages/server/src/db/QuickResponseRepository.js +14 -19
- package/packages/server/src/db/migrations/sessionsMigrations.js +61 -61
- package/packages/server/src/index.js +42 -29
- package/packages/server/src/services/commandRunner.js +52 -99
- package/packages/server/src/services/kanbanTriggers.js +83 -56
- package/packages/server/src/services/schedulerService.js +68 -44
- package/packages/server/src/services/sessionExecution.js +102 -61
- package/packages/server/src/services/sessionManager.js +63 -37
- package/packages/server/src/services/summaryService.js +56 -53
- package/packages/server/src/services/templateTriggerService.js +58 -31
- package/packages/server/src/ws/WebSocketManager.js +5 -0
- package/packages/web/dist/assets/ActiveSessionsView-Bd8FWObJ.js +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-DfYXc6dz.css +1 -0
- package/packages/web/dist/assets/{AgentLogsView-c42v_j_5.js → AgentLogsView-C4FTXUH8.js} +1 -1
- package/packages/web/dist/assets/{ArchiveConfirmModal-DBuOmtXu.js → ArchiveConfirmModal-MpqhAjWZ.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-CkKJ3Htz.js → CommandButtonDetailView-_tOjEGjk.js} +1 -1
- package/packages/web/dist/assets/{EffortLevelSelector-BHJHSqul.js → EffortLevelSelector-B4z3zfJ6.js} +1 -1
- package/packages/web/dist/assets/{GeneralSettingsView-CdxfteZ2.js → GeneralSettingsView-BZ4x6T_N.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-DabnHhE4.js → InterpolationHelp-CIlrD5JA.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-DBvC-1OX.js +2 -0
- package/packages/web/dist/assets/{ModelSelector-BWIU4ud7.js → ModelSelector-BTmHxqCs.js} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BIZl8QlH.js → NewSessionView-Cdgt6wnk.js} +2 -2
- package/packages/web/dist/assets/{PathChooser-nhat_Pz4.js → PathChooser-CPEkT0uu.js} +1 -1
- package/packages/web/dist/assets/{ProjectEditView-DD-2_VrW.js → ProjectEditView-D59hr-v2.js} +1 -1
- package/packages/web/dist/assets/{ProjectListView-BOWbfoXQ.js → ProjectListView-gG4AR1i9.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-DC4uvSn2.js → ProjectNewView-BPjn1O4f.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-C04jD9NZ.js +1 -0
- package/packages/web/dist/assets/{QuickResponseSettings-Bk9mq96x.js → QuickResponseSettings-DaEXIp3-.js} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-BRvcnkQr.js → QuickResponsesPanel-CIdblIbt.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-CwGM4P3c.js → ResizableTextarea-DJekfIXO.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-BGDVHU9u.js → SessionCard-CAdetaVH.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-D20G3bX8.css → SessionCard-CcqIjL8q.css} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-CHYrx2Ab.js → SessionDetailView-BOdnH-cW.js} +17 -17
- package/packages/web/dist/assets/{SessionDetailView-7bWgC7Es.css → SessionDetailView-mnGRMaLY.css} +1 -1
- package/packages/web/dist/assets/{SessionFormOptions-8qvL25ca.js → SessionFormOptions-Dy57kl-x.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-BAIBtJF7.css → SessionListView-78k6TTz6.css} +1 -1
- package/packages/web/dist/assets/SessionListView-DUMUXfp4.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-B-w3n4c3.js → SessionLogStream-DHPxkaaK.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-Dd0ZJ4Nv.js → SettingsView-BzrkWbH3.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-CzyLjsdJ.js → SlashCommandWizard-BAC_oF5i.js} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-DTbh7uAF.js → SummarySettingsView-J8BQBEe9.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-BOnhkdtH.js → TemplateDetailView-2ilWbANb.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-CY87n64i.js → commandButtons-aIO6hqZn.js} +1 -1
- package/packages/web/dist/assets/{index-DcA6pqXV.js → index-B6W39ctH.js} +1 -1
- package/packages/web/dist/assets/{index-NzLFVaCi.js → index-BGmIjKYB.js} +1 -1
- package/packages/web/dist/assets/{index-CO4EBOFw.js → index-BwChYYnJ.js} +1 -1
- package/packages/web/dist/assets/{index-BshkV3r5.js → index-CDRRIqmL.js} +1 -1
- package/packages/web/dist/assets/{index-Dx0sYW7H.js → index-CSA0abwg.js} +1 -1
- package/packages/web/dist/assets/{index-Ce6sL47U.js → index-CWTVEGZv.js} +1 -1
- package/packages/web/dist/assets/{index-i1o916sk.js → index-CjJX0Eli.js} +1 -1
- package/packages/web/dist/assets/{index-BRUlEEHm.js → index-CpNgrGiE.js} +1 -1
- package/packages/web/dist/assets/{index-gMpnPf1V.js → index-DCxYGijD.js} +1 -1
- package/packages/web/dist/assets/{index-jGjvGBfk.js → index-DF6g7nEj.js} +1 -1
- package/packages/web/dist/assets/{index-aCw-iXPX.js → index-DMl4xPIQ.js} +1 -1
- package/packages/web/dist/assets/{index--OtPwBbF.js → index-DMsWg7Ax.js} +3 -3
- package/packages/web/dist/assets/{index-CjHb9rXv.js → index-DePUHO3n.js} +1 -1
- package/packages/web/dist/assets/{index-C6m-WfqP.js → index-DvfYqZgb.js} +1 -1
- package/packages/web/dist/assets/{index-DkLkDgig.js → index-DxUd3T5E.js} +23 -23
- package/packages/web/dist/assets/{index-BXUcbV4K.js → index-GLkcnEcc.js} +1 -1
- package/packages/web/dist/assets/{index-DPwwgloE.js → index-ZDCxncSd.js} +1 -1
- package/packages/web/dist/assets/{index-DxboI9i-.js → index-f24-2-RT.js} +1 -1
- package/packages/web/dist/assets/{index-Bi4bQ_UB.js → index-uZfsnHcN.js} +1 -1
- package/packages/web/dist/assets/{projects-C2Y29PSJ.js → projects-DGF_kWVA.js} +1 -1
- package/packages/web/dist/assets/{providers-CeJXuo0Q.js → providers-Cm7emO-N.js} +1 -1
- package/packages/web/dist/assets/sessions-CH7PeypH.js +1 -0
- package/packages/web/dist/assets/{settings-BplIxCbi.js → settings-C7sXXJ-n.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/ActiveSessionsView-BryJ-V3f.js +0 -1
- package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +0 -1
- package/packages/web/dist/assets/MarkdownEditor-k4zBLGqU.js +0 -2
- package/packages/web/dist/assets/ProvidersView-DT5afh1V.js +0 -1
- package/packages/web/dist/assets/SessionListView-927Yq6Il.js +0 -1
- package/packages/web/dist/assets/sessions-CMby7ij3.js +0 -1
|
@@ -75,6 +75,70 @@ class SchedulerService {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Resolve skill/command invocations in the prompt, returning the effective prompt and system prompt.
|
|
80
|
+
* @param {object} session - Session object
|
|
81
|
+
* @param {string} workingDirectory - Working directory
|
|
82
|
+
* @param {string} projectSystemPrompt - Project-level system prompt
|
|
83
|
+
* @returns {Promise<{prompt: string, effectivePrompt: string, effectiveSystemPrompt: string, sessionAttachments: Array}>}
|
|
84
|
+
*/
|
|
85
|
+
async resolveScheduledPrompt(session, workingDirectory, projectSystemPrompt) {
|
|
86
|
+
const prompt = session.pendingPrompt.trim();
|
|
87
|
+
const sessionAttachments = attachments.getBySessionId(session.id);
|
|
88
|
+
|
|
89
|
+
const resolved = await slashCommandService.resolvePromptSkillOrCommand(
|
|
90
|
+
workingDirectory, prompt, projectSystemPrompt
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
prompt,
|
|
95
|
+
effectivePrompt: resolved ? resolved.userMessage : prompt,
|
|
96
|
+
effectiveSystemPrompt: resolved ? resolved.systemPrompt : projectSystemPrompt,
|
|
97
|
+
sessionAttachments,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle the fresh-session branch of startScheduledSession.
|
|
103
|
+
* Creates the initial user message, updates status, and starts the session.
|
|
104
|
+
* @param {object} session - Session object
|
|
105
|
+
* @param {string} prompt - Raw prompt text
|
|
106
|
+
* @param {string} effectivePrompt - Prompt after slash command resolution
|
|
107
|
+
* @param {string} effectiveSystemPrompt - System prompt after resolution
|
|
108
|
+
* @param {string} workingDirectory - Working directory
|
|
109
|
+
* @param {Array} sessionAttachments - Attachments for context
|
|
110
|
+
*/
|
|
111
|
+
async startFreshScheduledSession({ session, prompt, effectivePrompt, effectiveSystemPrompt, workingDirectory, sessionAttachments }) {
|
|
112
|
+
const activeConv = conversations.getActiveBySessionId(session.id);
|
|
113
|
+
if (!activeConv) {
|
|
114
|
+
throw new Error(`No active conversation found for session ${session.id}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create the initial user message
|
|
118
|
+
const userMessage = messages.create(session.id, 'user', prompt, { toolUse: null, conversationId: activeConv.id });
|
|
119
|
+
|
|
120
|
+
// Broadcast the new message so UI updates
|
|
121
|
+
broadcastToSession(session.id, WS_MESSAGE_TYPES.MESSAGE_CREATED, {
|
|
122
|
+
sessionId: session.id,
|
|
123
|
+
message: userMessage,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Update status from 'scheduled' to 'starting' and clear pendingPrompt
|
|
127
|
+
sessions.update(session.id, {
|
|
128
|
+
status: 'starting',
|
|
129
|
+
scheduledAt: null,
|
|
130
|
+
pendingPrompt: null,
|
|
131
|
+
});
|
|
132
|
+
broadcastToSession(session.id, WS_MESSAGE_TYPES.SESSION_STATUS, { sessionId: session.id, status: 'starting' });
|
|
133
|
+
|
|
134
|
+
await this.sessionManager.runSession(
|
|
135
|
+
session.id,
|
|
136
|
+
effectivePrompt,
|
|
137
|
+
workingDirectory,
|
|
138
|
+
{ systemPrompt: effectiveSystemPrompt, fileAttachments: sessionAttachments, model: session.pendingModel }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
78
142
|
/**
|
|
79
143
|
* Start a scheduled session
|
|
80
144
|
* @param {object} session - Session to start
|
|
@@ -100,27 +164,17 @@ class SchedulerService {
|
|
|
100
164
|
throw new Error(`No pendingPrompt found for session ${session.id}`);
|
|
101
165
|
}
|
|
102
166
|
|
|
103
|
-
|
|
167
|
+
// Resolve skill/command invocations
|
|
168
|
+
const { prompt, effectivePrompt, effectiveSystemPrompt, sessionAttachments } =
|
|
169
|
+
await this.resolveScheduledPrompt(session, workingDirectory, project.systemPrompt);
|
|
104
170
|
|
|
105
171
|
// Get the session messages to determine if this is initial or continuation
|
|
106
172
|
const sessionMessages = messages.getBySessionId(session.id);
|
|
107
173
|
const hasAssistantResponses = sessionMessages.some((msg) => msg.role === 'assistant');
|
|
108
174
|
|
|
109
|
-
// Get attachments for context
|
|
110
|
-
const sessionAttachments = attachments.getBySessionId(session.id);
|
|
111
|
-
|
|
112
|
-
// Resolve skill/command invocations so skill body goes into system prompt
|
|
113
|
-
const resolved = await slashCommandService.resolvePromptSkillOrCommand(
|
|
114
|
-
workingDirectory, prompt, project.systemPrompt
|
|
115
|
-
);
|
|
116
|
-
const effectivePrompt = resolved ? resolved.userMessage : prompt;
|
|
117
|
-
const effectiveSystemPrompt = resolved ? resolved.systemPrompt : project.systemPrompt;
|
|
118
|
-
|
|
119
175
|
// Determine if this is an initial run or a continuation
|
|
120
176
|
if (hasAssistantResponses) {
|
|
121
177
|
// Session has conversation history - this is a scheduled continuation
|
|
122
|
-
|
|
123
|
-
// Update status from 'scheduled' to 'starting' and clear pendingPrompt
|
|
124
178
|
sessions.update(session.id, {
|
|
125
179
|
status: 'starting',
|
|
126
180
|
scheduledAt: null,
|
|
@@ -136,37 +190,7 @@ class SchedulerService {
|
|
|
136
190
|
);
|
|
137
191
|
} else {
|
|
138
192
|
// Fresh session - initial run
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Get the active conversation
|
|
142
|
-
const activeConv = conversations.getActiveBySessionId(session.id);
|
|
143
|
-
if (!activeConv) {
|
|
144
|
-
throw new Error(`No active conversation found for session ${session.id}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Create the initial user message
|
|
148
|
-
const userMessage = messages.create(session.id, 'user', prompt, { toolUse: null, conversationId: activeConv.id });
|
|
149
|
-
|
|
150
|
-
// Broadcast the new message so UI updates
|
|
151
|
-
broadcastToSession(session.id, WS_MESSAGE_TYPES.MESSAGE_CREATED, {
|
|
152
|
-
sessionId: session.id,
|
|
153
|
-
message: userMessage,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Update status from 'scheduled' to 'starting' and clear pendingPrompt
|
|
157
|
-
sessions.update(session.id, {
|
|
158
|
-
status: 'starting',
|
|
159
|
-
scheduledAt: null,
|
|
160
|
-
pendingPrompt: null,
|
|
161
|
-
});
|
|
162
|
-
broadcastToSession(session.id, WS_MESSAGE_TYPES.SESSION_STATUS, { sessionId: session.id, status: 'starting' });
|
|
163
|
-
|
|
164
|
-
await this.sessionManager.runSession(
|
|
165
|
-
session.id,
|
|
166
|
-
effectivePrompt,
|
|
167
|
-
workingDirectory,
|
|
168
|
-
{ systemPrompt: effectiveSystemPrompt, fileAttachments: sessionAttachments, model: session.pendingModel }
|
|
169
|
-
);
|
|
193
|
+
await this.startFreshScheduledSession({ session, prompt, effectivePrompt, effectiveSystemPrompt, workingDirectory, sessionAttachments });
|
|
170
194
|
}
|
|
171
195
|
}
|
|
172
196
|
|
|
@@ -158,66 +158,14 @@ async function buildPromptForContinue(modelChanged, conversationId, promptWithAt
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @param {
|
|
164
|
-
* @param {string}
|
|
165
|
-
* @param {
|
|
166
|
-
* @
|
|
167
|
-
* @param {Object} config.callbacks - Callback functions from sessionManager
|
|
161
|
+
* Resolve model/provider and build session environment for a continue operation.
|
|
162
|
+
* Also detects model changes and updates the session record.
|
|
163
|
+
* @param {Object} session - Current session object
|
|
164
|
+
* @param {string} sessionId - Session ID
|
|
165
|
+
* @param {string|null} model - Requested model (null to keep current)
|
|
166
|
+
* @returns {{ effectiveModel: string|null, sessionEnv: Object, modelChanged: boolean, session: Object }}
|
|
168
167
|
*/
|
|
169
|
-
|
|
170
|
-
const { options = {}, callbacks } = config;
|
|
171
|
-
const { systemPrompt = null, fileAttachments = [], model = null } = options;
|
|
172
|
-
// Check if session is already running
|
|
173
|
-
if (activeSessions.has(sessionId)) {
|
|
174
|
-
throw new Error('Session is already processing');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Get the session to retrieve the Claude session ID and settings
|
|
178
|
-
let session = sessions.getById(sessionId);
|
|
179
|
-
if (!session) {
|
|
180
|
-
throw new Error('Session not found');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const controller = new AbortController();
|
|
184
|
-
activeSessions.set(sessionId, { controller });
|
|
185
|
-
|
|
186
|
-
// Ensure there's an active conversation for this session
|
|
187
|
-
const activeConversation = conversations.ensureActiveConversation(sessionId);
|
|
188
|
-
activeConversationIds.set(sessionId, activeConversation.id);
|
|
189
|
-
console.log(`[SESSION] continueSession: ensured active conversation ${activeConversation.id} for session ${sessionId}`);
|
|
190
|
-
|
|
191
|
-
// Each conversation has its own Claude session context
|
|
192
|
-
// If null, Claude will start a fresh session (no resume)
|
|
193
|
-
|
|
194
|
-
// Store the user message with conversation ID
|
|
195
|
-
const { broadcastToSession } = await import('../websocket.js');
|
|
196
|
-
const { WS_MESSAGE_TYPES } = await import('@circuschief/shared');
|
|
197
|
-
const message = messages.create(sessionId, 'user', content, { toolUse: null, conversationId: activeConversation.id });
|
|
198
|
-
console.log(`[SESSION] continueSession: created user message ${message.id} in conversation ${activeConversation.id}`);
|
|
199
|
-
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_MESSAGE, {
|
|
200
|
-
message,
|
|
201
|
-
conversationId: activeConversation.id, // Include conversation context
|
|
202
|
-
});
|
|
203
|
-
console.log(`[SESSION] continueSession: broadcast user message ${message.id} to conversation ${activeConversation.id}`);
|
|
204
|
-
|
|
205
|
-
// Associate any pending attachments with the message
|
|
206
|
-
if (fileAttachments.length > 0) {
|
|
207
|
-
attachments.updateMessageIdForSession(sessionId, message.id);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Build prompt with attachment context
|
|
211
|
-
const promptWithAttachments = buildPromptWithAttachments(content, fileAttachments);
|
|
212
|
-
|
|
213
|
-
// Update status to running
|
|
214
|
-
sessions.update(sessionId, { status: 'running' });
|
|
215
|
-
broadcastSessionStatus(sessionId, 'running');
|
|
216
|
-
|
|
217
|
-
// Create agent via gateway (or mock agent in mock mode)
|
|
218
|
-
const agentType = session.agentType || 'claude-code';
|
|
219
|
-
const agent = createAgentForSession(agentType);
|
|
220
|
-
|
|
168
|
+
function buildContinueModelAndEnv(session, sessionId, model) {
|
|
221
169
|
// Resolve the effective model: fall back to session.model so that resuming
|
|
222
170
|
// without an explicit model still resolves the correct provider (e.g.
|
|
223
171
|
// third-party base URL and auth tokens).
|
|
@@ -230,15 +178,29 @@ export async function continueSessionCore(sessionId, content, workingDirectory,
|
|
|
230
178
|
// Check if model changed from the session's last requested model
|
|
231
179
|
// When model changes, we can't resume the previous session - thinking blocks and
|
|
232
180
|
// session context may be incompatible between different models/providers
|
|
233
|
-
const modelChanged = model && session.model && model !== session.model;
|
|
181
|
+
const modelChanged = Boolean(model && session.model && model !== session.model);
|
|
234
182
|
|
|
235
183
|
// Update session.model to track the user-requested model (short format)
|
|
236
184
|
// This must happen AFTER modelChanged detection so we compare old vs new
|
|
185
|
+
let updatedSession = session;
|
|
237
186
|
if (model) {
|
|
238
187
|
sessions.update(sessionId, { model });
|
|
239
|
-
|
|
188
|
+
updatedSession = sessions.getById(sessionId); // refresh
|
|
240
189
|
}
|
|
241
190
|
|
|
191
|
+
return { effectiveModel, sessionEnv, modelChanged, session: updatedSession };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build query params and agent call meta for a continue session operation.
|
|
196
|
+
* @param {Object} opts
|
|
197
|
+
* @returns {{ queryParams: Object, agentCallMeta: Object }}
|
|
198
|
+
*/
|
|
199
|
+
async function buildContinueParams({
|
|
200
|
+
sessionId, session, model, systemPrompt, effectiveModel, sessionEnv,
|
|
201
|
+
modelChanged, activeConversation, promptWithAttachments,
|
|
202
|
+
workingDirectory, controller, agentType,
|
|
203
|
+
}) {
|
|
242
204
|
// Only resume if we have a session ID AND model hasn't changed
|
|
243
205
|
const canResume = activeConversation.claudeSessionId && !modelChanged;
|
|
244
206
|
|
|
@@ -269,6 +231,85 @@ export async function continueSessionCore(sessionId, content, workingDirectory,
|
|
|
269
231
|
promptLength: promptWithContext.length,
|
|
270
232
|
};
|
|
271
233
|
|
|
234
|
+
return { queryParams, agentCallMeta };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Set up the active conversation, create the user message, broadcast it,
|
|
239
|
+
* associate attachments, and build the prompt with attachment context.
|
|
240
|
+
* @returns {{ activeConversation: Object, promptWithAttachments: string }}
|
|
241
|
+
*/
|
|
242
|
+
async function setupConversationAndMessage(sessionId, content, fileAttachments) {
|
|
243
|
+
const activeConversation = conversations.ensureActiveConversation(sessionId);
|
|
244
|
+
activeConversationIds.set(sessionId, activeConversation.id);
|
|
245
|
+
|
|
246
|
+
const { broadcastToSession } = await import('../websocket.js');
|
|
247
|
+
const { WS_MESSAGE_TYPES } = await import('@circuschief/shared');
|
|
248
|
+
const message = messages.create(sessionId, 'user', content, { toolUse: null, conversationId: activeConversation.id });
|
|
249
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_MESSAGE, {
|
|
250
|
+
message,
|
|
251
|
+
conversationId: activeConversation.id,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (fileAttachments.length > 0) {
|
|
255
|
+
attachments.updateMessageIdForSession(sessionId, message.id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const promptWithAttachments = buildPromptWithAttachments(content, fileAttachments);
|
|
259
|
+
return { activeConversation, promptWithAttachments };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Continue a session with a follow-up message (core implementation)
|
|
264
|
+
* @param {string} sessionId
|
|
265
|
+
* @param {string} content
|
|
266
|
+
* @param {string} workingDirectory
|
|
267
|
+
* @param {Object} config - Session options and callbacks
|
|
268
|
+
* @param {Object} [config.options] - Session options (systemPrompt, fileAttachments, model)
|
|
269
|
+
* @param {Object} config.callbacks - Callback functions from sessionManager
|
|
270
|
+
*/
|
|
271
|
+
export async function continueSessionCore(sessionId, content, workingDirectory, config = {}) {
|
|
272
|
+
const { options = {}, callbacks } = config;
|
|
273
|
+
const { systemPrompt = null, fileAttachments = [], model = null } = options;
|
|
274
|
+
// Check if session is already running
|
|
275
|
+
if (activeSessions.has(sessionId)) {
|
|
276
|
+
throw new Error('Session is already processing');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Get the session to retrieve the Claude session ID and settings
|
|
280
|
+
let session = sessions.getById(sessionId);
|
|
281
|
+
if (!session) {
|
|
282
|
+
throw new Error('Session not found');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const controller = new AbortController();
|
|
286
|
+
activeSessions.set(sessionId, { controller });
|
|
287
|
+
|
|
288
|
+
// Ensure there's an active conversation and create the user message
|
|
289
|
+
const { activeConversation, promptWithAttachments } = await setupConversationAndMessage(
|
|
290
|
+
sessionId, content, fileAttachments
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Update status to running
|
|
294
|
+
sessions.update(sessionId, { status: 'running' });
|
|
295
|
+
broadcastSessionStatus(sessionId, 'running');
|
|
296
|
+
|
|
297
|
+
// Create agent via gateway (or mock agent in mock mode)
|
|
298
|
+
const agentType = session.agentType || 'claude-code';
|
|
299
|
+
const agent = createAgentForSession(agentType);
|
|
300
|
+
|
|
301
|
+
// Resolve model/provider and detect model changes
|
|
302
|
+
const modelEnv = buildContinueModelAndEnv(session, sessionId, model);
|
|
303
|
+
session = modelEnv.session;
|
|
304
|
+
|
|
305
|
+
// Build query params and agent call meta
|
|
306
|
+
const { queryParams, agentCallMeta } = await buildContinueParams({
|
|
307
|
+
sessionId, session, model, systemPrompt,
|
|
308
|
+
effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
|
|
309
|
+
modelChanged: modelEnv.modelChanged, activeConversation, promptWithAttachments,
|
|
310
|
+
workingDirectory, controller, agentType,
|
|
311
|
+
});
|
|
312
|
+
|
|
272
313
|
await _executeSession({
|
|
273
314
|
sessionId,
|
|
274
315
|
agent,
|
|
@@ -217,50 +217,39 @@ function validateAndFetchContinueContext(sessionId, conversationId) {
|
|
|
217
217
|
return { session, conversation, lastUserMessage };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Make sure this conversation is active
|
|
230
|
-
if (!conversation.isActive) {
|
|
231
|
-
conversations.update(conversationId, { isActive: true });
|
|
232
|
-
}
|
|
233
|
-
activeConversationIds.set(sessionId, conversationId);
|
|
234
|
-
|
|
235
|
-
// Update status to running
|
|
236
|
-
sessions.update(sessionId, { status: 'running' });
|
|
237
|
-
broadcastSessionStatus(sessionId, 'running');
|
|
238
|
-
|
|
239
|
-
// Use the existing user message content as the prompt
|
|
240
|
-
// Note: We do NOT create a new user message here - it already exists
|
|
241
|
-
|
|
242
|
-
// Create agent via gateway (or mock agent in mock mode)
|
|
243
|
-
const agentType = session.agentType || 'claude-code';
|
|
244
|
-
const agent = createAgentForSession(agentType);
|
|
245
|
-
|
|
246
|
-
// Resolve the effective model: fall back to session.model so that resuming
|
|
247
|
-
// without an explicit model still resolves the correct provider (e.g.
|
|
248
|
-
// third-party base URL and auth tokens).
|
|
220
|
+
/**
|
|
221
|
+
* Resolve the effective model, provider, and session env from a model override.
|
|
222
|
+
* Detects model changes and updates the session record when needed.
|
|
223
|
+
* @param {Object} session - Current session object
|
|
224
|
+
* @param {string} sessionId - Session ID
|
|
225
|
+
* @param {string|null} model - Requested model (null to keep current)
|
|
226
|
+
* @returns {{ effectiveModel: string|null, sessionEnv: Object, modelChanged: boolean, session: Object }}
|
|
227
|
+
*/
|
|
228
|
+
function buildModelAndProvider(session, sessionId, model) {
|
|
249
229
|
const effectiveModel = model || session.model;
|
|
250
|
-
|
|
251
|
-
// Derive provider from the effective model ID (returns null for Anthropic/SDK defaults)
|
|
252
230
|
const provider = resolveProviderFromModel(effectiveModel);
|
|
253
231
|
const sessionEnv = buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel);
|
|
232
|
+
const modelChanged = Boolean(model && session.model && model !== session.model);
|
|
254
233
|
|
|
255
|
-
|
|
256
|
-
const modelChanged = model && session.model && model !== session.model;
|
|
257
|
-
|
|
258
|
-
// Update session.model after detecting change
|
|
234
|
+
let updatedSession = session;
|
|
259
235
|
if (model) {
|
|
260
236
|
sessions.update(sessionId, { model });
|
|
261
|
-
|
|
237
|
+
updatedSession = sessions.getById(sessionId);
|
|
262
238
|
}
|
|
263
239
|
|
|
240
|
+
return { effectiveModel, sessionEnv, modelChanged, session: updatedSession };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build query params for continueSessionWithExistingMessage.
|
|
245
|
+
* Handles context building (model switch / branch) and resume detection.
|
|
246
|
+
* @returns {{ queryParams: Object, agentCallMeta: Object }}
|
|
247
|
+
*/
|
|
248
|
+
function buildExistingMessageQueryParams({
|
|
249
|
+
sessionId, conversationId, session, model, systemPrompt,
|
|
250
|
+
effectiveModel, sessionEnv, modelChanged, conversation,
|
|
251
|
+
lastUserMessage, workingDirectory, controller, agentType,
|
|
252
|
+
}) {
|
|
264
253
|
// Determine context needs and build context
|
|
265
254
|
const { needsContext, contextType } = determineContextNeed(conversation, modelChanged);
|
|
266
255
|
if (needsContext) {
|
|
@@ -284,7 +273,6 @@ export async function continueSessionWithExistingMessage(sessionId, conversation
|
|
|
284
273
|
resumeSessionId: canResume ? conversation.claudeSessionId : null,
|
|
285
274
|
});
|
|
286
275
|
|
|
287
|
-
// Logging metadata for agent call tracking
|
|
288
276
|
const agentCallMeta = {
|
|
289
277
|
sessionId,
|
|
290
278
|
conversationId,
|
|
@@ -296,6 +284,44 @@ export async function continueSessionWithExistingMessage(sessionId, conversation
|
|
|
296
284
|
promptLength: promptWithContext.length,
|
|
297
285
|
};
|
|
298
286
|
|
|
287
|
+
return { queryParams, agentCallMeta };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function continueSessionWithExistingMessage(sessionId, conversationId, workingDirectory, options = {}) {
|
|
291
|
+
const { systemPrompt = null, model = null } = options;
|
|
292
|
+
const context = validateAndFetchContinueContext(sessionId, conversationId);
|
|
293
|
+
let session = context.session;
|
|
294
|
+
const { conversation, lastUserMessage } = context;
|
|
295
|
+
|
|
296
|
+
const controller = new AbortController();
|
|
297
|
+
activeSessions.set(sessionId, { controller });
|
|
298
|
+
|
|
299
|
+
// Make sure this conversation is active
|
|
300
|
+
if (!conversation.isActive) {
|
|
301
|
+
conversations.update(conversationId, { isActive: true });
|
|
302
|
+
}
|
|
303
|
+
activeConversationIds.set(sessionId, conversationId);
|
|
304
|
+
|
|
305
|
+
// Update status to running
|
|
306
|
+
sessions.update(sessionId, { status: 'running' });
|
|
307
|
+
broadcastSessionStatus(sessionId, 'running');
|
|
308
|
+
|
|
309
|
+
// Create agent via gateway (or mock agent in mock mode)
|
|
310
|
+
const agentType = session.agentType || 'claude-code';
|
|
311
|
+
const agent = createAgentForSession(agentType);
|
|
312
|
+
|
|
313
|
+
// Resolve model/provider and detect model changes
|
|
314
|
+
const modelEnv = buildModelAndProvider(session, sessionId, model);
|
|
315
|
+
session = modelEnv.session;
|
|
316
|
+
|
|
317
|
+
// Build query params and agent call meta
|
|
318
|
+
const { queryParams, agentCallMeta } = buildExistingMessageQueryParams({
|
|
319
|
+
sessionId, conversationId, session, model, systemPrompt,
|
|
320
|
+
effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
|
|
321
|
+
modelChanged: modelEnv.modelChanged, conversation,
|
|
322
|
+
lastUserMessage, workingDirectory, controller, agentType,
|
|
323
|
+
});
|
|
324
|
+
|
|
299
325
|
await _executeSession({
|
|
300
326
|
sessionId,
|
|
301
327
|
agent,
|
|
@@ -36,6 +36,9 @@ import { isSummaryStale } from './summaryStaleCheck.js';
|
|
|
36
36
|
// Create the concurrency guard instance for summary generation
|
|
37
37
|
const guard = createConcurrencyGuard();
|
|
38
38
|
|
|
39
|
+
// Track scheduled CI-check timers so they can be cleared on shutdown
|
|
40
|
+
const activeTimers = new Set();
|
|
41
|
+
|
|
39
42
|
/**
|
|
40
43
|
* Generate summary for a session using Claude Code SDK (with concurrency guard)
|
|
41
44
|
* Only one generation can be in-flight per session at a time. If a generation is already
|
|
@@ -252,30 +255,45 @@ function createMinimalSummary(sessionId, session, allMessages) {
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
/**
|
|
255
|
-
*
|
|
258
|
+
* Retry summary generation if parsing failed and retries remain.
|
|
259
|
+
* Returns { shouldRetry: true, result } if a retry was performed,
|
|
260
|
+
* or { shouldRetry: false } if no retry is needed.
|
|
261
|
+
* @param {Object} summaryData - Parsed summary data (may have _parseFailed flag)
|
|
262
|
+
* @param {number} retryCount - Current retry count
|
|
256
263
|
* @param {string} sessionId
|
|
257
|
-
* @param {number} retryCount
|
|
258
264
|
* @param {boolean} force
|
|
259
265
|
* @param {boolean} userInitiated
|
|
260
|
-
* @returns {Promise<Object|null>}
|
|
266
|
+
* @returns {Promise<{ shouldRetry: boolean, result?: Object|null }>}
|
|
261
267
|
*/
|
|
268
|
+
async function retryIfParseFailed(summaryData, retryCount, { sessionId, force, userInitiated }) {
|
|
269
|
+
if (!summaryData._parseFailed || retryCount >= MAX_RETRIES) {
|
|
270
|
+
return { shouldRetry: false };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(
|
|
274
|
+
`[SummaryService] Parse failed for session ${sessionId}, retrying (attempt ${retryCount + 2}/${MAX_RETRIES + 1})`
|
|
275
|
+
);
|
|
276
|
+
const backoffMs = 1000 * (retryCount + 1);
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
278
|
+
const result = await _doGenerateSummary(sessionId, retryCount + 1, force, userInitiated);
|
|
279
|
+
return { shouldRetry: true, result };
|
|
280
|
+
}
|
|
281
|
+
|
|
262
282
|
async function _doGenerateSummary(sessionId, retryCount = 0, force = false, userInitiated = false) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (check.skip) return check.result;
|
|
283
|
+
// Early-exit checks
|
|
284
|
+
const check = shouldGenerateSummary(sessionId, force, userInitiated);
|
|
285
|
+
if (check.skip) return check.result;
|
|
267
286
|
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
const { session, globalSettings, existingSummary, allMessages } = check;
|
|
288
|
+
const recentMessages = allMessages.slice(-MAX_MESSAGES);
|
|
270
289
|
|
|
271
|
-
|
|
272
|
-
|
|
290
|
+
// Broadcast that we're generating (do this early so UI always gets the event)
|
|
291
|
+
broadcastGeneratingStatus(sessionId, true);
|
|
273
292
|
|
|
293
|
+
try {
|
|
274
294
|
// Handle sessions with too few messages
|
|
275
295
|
if (allMessages.length < MIN_MESSAGES_FOR_SUMMARY) {
|
|
276
|
-
|
|
277
|
-
broadcastGeneratingStatus(sessionId, false);
|
|
278
|
-
return summary;
|
|
296
|
+
return createMinimalSummary(sessionId, session, allMessages);
|
|
279
297
|
}
|
|
280
298
|
|
|
281
299
|
// Build conversation context and prompt
|
|
@@ -287,25 +305,14 @@ async function _doGenerateSummary(sessionId, retryCount = 0, force = false, user
|
|
|
287
305
|
systemPrompt: SUMMARY_SYSTEM_PROMPT,
|
|
288
306
|
});
|
|
289
307
|
|
|
290
|
-
// Parse response
|
|
308
|
+
// Parse response and retry if needed
|
|
291
309
|
const summaryData = parseSummaryResponse(responseText);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (summaryData._parseFailed && retryCount < MAX_RETRIES) {
|
|
295
|
-
console.log(
|
|
296
|
-
`[SummaryService] Parse failed for session ${sessionId}, retrying (attempt ${retryCount + 2}/${MAX_RETRIES + 1})`
|
|
297
|
-
);
|
|
298
|
-
const backoffMs = 1000 * (retryCount + 1);
|
|
299
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
300
|
-
return _doGenerateSummary(sessionId, retryCount + 1, force, userInitiated);
|
|
301
|
-
}
|
|
310
|
+
const retryResult = await retryIfParseFailed(summaryData, retryCount, { sessionId, force, userInitiated });
|
|
311
|
+
if (retryResult.shouldRetry) return retryResult.result;
|
|
302
312
|
|
|
303
313
|
// Persist summary and broadcast updates
|
|
304
314
|
const summary = await saveSummaryResult(sessionId, summaryData, session, allMessages);
|
|
305
315
|
|
|
306
|
-
// Clear the generating flag so the UI knows generation is complete
|
|
307
|
-
broadcastGeneratingStatus(sessionId, false);
|
|
308
|
-
|
|
309
316
|
console.log(`[SummaryService] Successfully generated summary for session ${sessionId}`);
|
|
310
317
|
|
|
311
318
|
// Propagate summary updates to parent sessions (workflow-aware)
|
|
@@ -320,11 +327,9 @@ async function _doGenerateSummary(sessionId, retryCount = 0, force = false, user
|
|
|
320
327
|
stack: error.stack,
|
|
321
328
|
sessionId,
|
|
322
329
|
});
|
|
323
|
-
|
|
324
|
-
// Broadcast that generation stopped
|
|
325
|
-
broadcastGeneratingStatus(sessionId, false);
|
|
326
|
-
|
|
327
330
|
return null;
|
|
331
|
+
} finally {
|
|
332
|
+
broadcastGeneratingStatus(sessionId, false);
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
|
|
@@ -366,14 +371,17 @@ export function onSessionActivity(sessionId) {
|
|
|
366
371
|
* @param {string} sessionId
|
|
367
372
|
*/
|
|
368
373
|
function scheduleCiChecks(sessionId) {
|
|
369
|
-
const
|
|
374
|
+
const makeCheck = (timerId) => async () => {
|
|
375
|
+
activeTimers.delete(timerId);
|
|
370
376
|
const prStatusService = await import('./prStatusService.js');
|
|
371
377
|
prStatusService.checkSessionCiStatusNow(sessionId);
|
|
372
378
|
};
|
|
373
379
|
// Check after 2 minutes (CI often takes a few minutes)
|
|
374
|
-
setTimeout(
|
|
380
|
+
const timerId1 = setTimeout(() => makeCheck(timerId1)(), 2 * 60 * 1000);
|
|
381
|
+
activeTimers.add(timerId1);
|
|
375
382
|
// Check again after 5 minutes
|
|
376
|
-
setTimeout(
|
|
383
|
+
const timerId2 = setTimeout(() => makeCheck(timerId2)(), 5 * 60 * 1000);
|
|
384
|
+
activeTimers.add(timerId2);
|
|
377
385
|
}
|
|
378
386
|
|
|
379
387
|
/**
|
|
@@ -523,30 +531,25 @@ export function propagatePrUrlToParent(sessionId, prUrl) {
|
|
|
523
531
|
}
|
|
524
532
|
|
|
525
533
|
// Re-export from extracted modules for backward compatibility
|
|
526
|
-
// These are used by external consumers and tests
|
|
527
534
|
export {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
MAX_RETRIES,
|
|
532
|
-
DEFAULT_SESSION_TITLE_PROMPT,
|
|
533
|
-
SUMMARY_SYSTEM_PROMPT,
|
|
534
|
-
formatMessages,
|
|
535
|
-
buildIncrementalPrompt,
|
|
536
|
-
parseSummaryResponse,
|
|
537
|
-
stripMarkdownCodeBlock as _stripMarkdownCodeBlock,
|
|
538
|
-
trackMessageMetadata as _trackMessageMetadata,
|
|
535
|
+
MAX_MESSAGES, MIN_MESSAGES_FOR_SUMMARY, MAX_RETRIES, DEFAULT_SESSION_TITLE_PROMPT,
|
|
536
|
+
SUMMARY_SYSTEM_PROMPT, formatMessages, buildIncrementalPrompt, parseSummaryResponse,
|
|
537
|
+
stripMarkdownCodeBlock as _stripMarkdownCodeBlock, trackMessageMetadata as _trackMessageMetadata,
|
|
539
538
|
};
|
|
540
|
-
|
|
541
|
-
// From summaryClaudeClient.js
|
|
542
539
|
export { callClaude };
|
|
543
|
-
|
|
544
|
-
// From prUrlService.js
|
|
545
540
|
export { parsePrUrl, validatePrUrl, extractPrUrlIfNeeded, enrichPrData as _enrichPrData };
|
|
546
|
-
|
|
547
|
-
// From childSessionContext.js
|
|
548
541
|
export { getChildSessions, buildChildSessionContext, aggregateFilesModified };
|
|
549
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Clear all pending CI-check timers (called during graceful shutdown).
|
|
545
|
+
*/
|
|
546
|
+
export function clearScheduledTimers() {
|
|
547
|
+
for (const id of activeTimers) {
|
|
548
|
+
clearTimeout(id);
|
|
549
|
+
}
|
|
550
|
+
activeTimers.clear();
|
|
551
|
+
}
|
|
552
|
+
|
|
550
553
|
// Read-only accessors for concurrency guard state
|
|
551
554
|
export const isGenerationActive = (key) => guard.isActive(key);
|
|
552
555
|
export const isRegenerationPending = (key) => guard.isPending(key);
|