circuschief 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/server/src/api/projects-session-helpers.js +25 -0
- package/packages/server/src/api/projects.js +1 -0
- package/packages/server/src/api/sessions-draft.js +1 -0
- package/packages/server/src/api/settings.js +52 -4
- package/packages/server/src/db/ConversationRepository.js +16 -3
- package/packages/server/src/db/ProjectDefaultsRepository.js +47 -37
- package/packages/server/src/db/SessionRepository.js +12 -8
- package/packages/server/src/db/SettingsRepository.js +44 -16
- package/packages/server/src/db/conversation-helpers.js +1 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +4 -0
- package/packages/server/src/db/migrations/index.js +2 -0
- package/packages/server/src/db/migrations/sessionsMigrations.js +6 -1
- package/packages/server/src/db/session-helpers.js +3 -0
- package/packages/server/src/schema.sql +8 -0
- package/packages/server/src/services/agentCallLogger.js +1 -1
- package/packages/server/src/services/commandButtonPrompts.js +48 -0
- package/packages/server/src/services/draftSessionService.js +2 -0
- package/packages/server/src/services/sessionExecution.js +3 -1
- package/packages/server/src/services/sessionPrompts.js +4 -0
- package/packages/server/src/services/sessionProvider.js +6 -8
- package/packages/server/src/services/streamEventCallbacks.js +72 -40
- package/packages/server/src/services/streamEventHandler.js +13 -2
- package/packages/server/src/services/streamUsageHandler.js +6 -0
- package/packages/server/src/services/summaryClaudeClient.js +37 -12
- package/packages/server/src/services/summaryModelClient.js +154 -0
- package/packages/server/src/services/summaryModelResolver.js +148 -0
- package/packages/server/src/services/summaryService.js +6 -4
- package/packages/server/src/services/usageTracker.js +5 -1
- package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
- package/packages/shared/src/constants.js +1 -2
- package/packages/shared/src/contracts/projects.js +2 -0
- package/packages/shared/src/utils.js +9 -17
- package/packages/web/dist/assets/ActiveSessionsView-UJsCILDL.js +1 -0
- package/packages/web/dist/assets/{AgentLogsView-Cdw4nmvd.js → AgentLogsView-BGFPLjLa.js} +1 -1
- package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +1 -0
- package/packages/web/dist/assets/{ArchiveConfirmModal-J48eh3zw.js → ArchiveConfirmModal-OFaj_uX5.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-DnFhJY5A.js → CommandButtonDetailView-D8S258uP.js} +1 -1
- package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-CQkmdczf.js → GeneralSettingsView-DsHChEhv.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-XyM3k6lN.js → InputWithButton-Ci15ox0a.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-PfYR3KJo.js → InterpolationHelp-CIkOSkWX.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +2 -0
- package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +1 -0
- package/packages/web/dist/assets/{ModelSelector-BZOT1Jc6.css → ModelSelector-D8hbTRIt.css} +1 -1
- package/packages/web/dist/assets/{NewSessionView-DkjFLvHU.js → NewSessionView-BCqtIgWH.js} +2 -2
- package/packages/web/dist/assets/{NewSessionView-D_Hi7M9g.css → NewSessionView-CUUdHkfv.css} +1 -1
- package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +1 -0
- package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +1 -0
- package/packages/web/dist/assets/{ProjectListView-CuYMmd3O.js → ProjectListView-B9FuWESY.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-CNaA4Maf.js → ProjectNewView-D62jYlBL.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +1 -0
- package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-BqMYSHb0.js → QuickResponsesPanel-DZ_Lre_l.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-wYF3K2RO.js → ResizableTextarea-DiIOEGjN.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-bLaQEWWX.js → SessionCard-DmjnVYWn.js} +1 -1
- package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-DYUISplS.js +1 -0
- package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-DTnDAF95.js → SessionLogStream-DpUE6Xsh.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-DNLUSsHV.js → SettingsView-BC055tIA.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-CRGFaO8t.js → SlashCommandWizard-DmTyNG9O.js} +1 -1
- package/packages/web/dist/assets/SummarySettingsView-BgnRCwlq.js +1 -0
- package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
- package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +1 -0
- package/packages/web/dist/assets/{commandButtons-Bbjf3fCt.js → commandButtons-D4RPpLiu.js} +1 -1
- package/packages/web/dist/assets/index-4rhEeO0B.js +1 -0
- package/packages/web/dist/assets/index-9vb2KaAd.js +1 -0
- package/packages/web/dist/assets/index-B0CvZXuN.js +7 -0
- package/packages/web/dist/assets/{index-BQL_L4gL.js → index-B6G18FqB.js} +14 -14
- package/packages/web/dist/assets/{index-Cf6vdW-B.js → index-BGwH4Cfn.js} +3 -3
- package/packages/web/dist/assets/index-BUhvkAdF.js +1 -0
- package/packages/web/dist/assets/index-BcnkUk2o.js +1 -0
- package/packages/web/dist/assets/{index-Bs7Qf5D6.js → index-Bn5xdGFM.js} +2 -2
- package/packages/web/dist/assets/index-CNwkdB0T.js +1 -0
- package/packages/web/dist/assets/index-CfL84oGW.js +1 -0
- package/packages/web/dist/assets/index-CkmxO8Mm.js +1 -0
- package/packages/web/dist/assets/index-Cpy4-yv3.js +1 -0
- package/packages/web/dist/assets/index-CrAQJmoZ.js +1 -0
- package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-Cs2nxhrT.css} +1 -1
- package/packages/web/dist/assets/index-D6Ky9vJe.js +3 -0
- package/packages/web/dist/assets/index-DfrE0gAC.js +1 -0
- package/packages/web/dist/assets/index-KwEyz0F3.js +1 -0
- package/packages/web/dist/assets/index-OfCywayk.js +1 -0
- package/packages/web/dist/assets/index-PDesaJc6.js +1 -0
- package/packages/web/dist/assets/index-uB6nhSvz.js +1 -0
- package/packages/web/dist/assets/{projects-CPt3AB7U.js → projects-BUiOGmmb.js} +1 -1
- package/packages/web/dist/assets/{providers-ChfeMvUq.js → providers-Bh1ZiiJi.js} +1 -1
- package/packages/web/dist/assets/sessions-DH1R-NhV.js +1 -0
- package/packages/web/dist/assets/settings-Z4AVVmkJ.js +1 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/ActiveSessionsView-UCbQrF1b.js +0 -1
- package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +0 -2
- package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
- package/packages/web/dist/assets/ProjectEditView-embVT7NC.js +0 -1
- package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +0 -1
- package/packages/web/dist/assets/QuickResponseSettings-BTQEKhwJ.js +0 -1
- package/packages/web/dist/assets/SessionDetailView-Cv-xMzXp.css +0 -1
- package/packages/web/dist/assets/SessionDetailView-CvQOUsW2.js +0 -36
- package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +0 -1
- package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +0 -1
- package/packages/web/dist/assets/SessionListView-Dranfb72.js +0 -1
- package/packages/web/dist/assets/SummarySettingsView-C7G_suHp.js +0 -1
- package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
- package/packages/web/dist/assets/TemplateDetailView-B78_DLMR.js +0 -1
- package/packages/web/dist/assets/index--V7c-VZf.js +0 -1
- package/packages/web/dist/assets/index-8Q04yd7H.js +0 -1
- package/packages/web/dist/assets/index-B47XRBDH.js +0 -1
- package/packages/web/dist/assets/index-BXbgZrhS.js +0 -1
- package/packages/web/dist/assets/index-CGhDVPen.js +0 -1
- package/packages/web/dist/assets/index-CKcRO1A6.js +0 -1
- package/packages/web/dist/assets/index-CTq-SLIW.js +0 -1
- package/packages/web/dist/assets/index-CYyos3iC.js +0 -1
- package/packages/web/dist/assets/index-CsCREAxF.js +0 -1
- package/packages/web/dist/assets/index-DJTTk_8T.js +0 -3
- package/packages/web/dist/assets/index-DPqUJ5JK.js +0 -1
- package/packages/web/dist/assets/index-EwAe1dKg.js +0 -1
- package/packages/web/dist/assets/index-JBA8axyA.js +0 -1
- package/packages/web/dist/assets/index-JkVHFtK5.js +0 -7
- package/packages/web/dist/assets/index-gMPUwT55.js +0 -1
- package/packages/web/dist/assets/index-wadc_0zT.js +0 -1
- package/packages/web/dist/assets/sessions-CwPsJOb1.js +0 -1
- package/packages/web/dist/assets/settings-BOj6wq6t.js +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { commandButtons } from '../database.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build Command Button API instructions for system prompt if the project has command buttons.
|
|
5
|
+
* @param {string} apiUrl - Base API URL
|
|
6
|
+
* @param {string} sessionId - Current session ID
|
|
7
|
+
* @param {string} projectId - Current project ID
|
|
8
|
+
* @returns {string} Command button instructions or empty string if no buttons configured
|
|
9
|
+
*/
|
|
10
|
+
export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
|
|
11
|
+
const buttons = commandButtons.getByProjectId(projectId);
|
|
12
|
+
if (!buttons || buttons.length === 0) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `## Command Buttons API
|
|
17
|
+
|
|
18
|
+
This project has command buttons configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
|
|
19
|
+
|
|
20
|
+
### List Available Buttons
|
|
21
|
+
\`\`\`bash
|
|
22
|
+
curl ${apiUrl}/api/projects/${projectId}/command-buttons
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
### Run a Button
|
|
26
|
+
\`\`\`bash
|
|
27
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
Response: { runId, buttonId, status: "running", output: "" }
|
|
31
|
+
|
|
32
|
+
### Check Run Status & Output
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
|
|
38
|
+
|
|
39
|
+
### List All Runs for This Session
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
### Kill a Running Command
|
|
45
|
+
\`\`\`bash
|
|
46
|
+
curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
|
|
47
|
+
\`\`\``;
|
|
48
|
+
}
|
|
@@ -112,6 +112,7 @@ function getOrCreateInitialMessage(session, options) {
|
|
|
112
112
|
* @param {object} options
|
|
113
113
|
* @param {string} [options.prompt] - Optional new prompt to use/override
|
|
114
114
|
* @param {string} [options.model] - Optional model override
|
|
115
|
+
* @param {string|null} [options.providerId] - Optional provider override
|
|
115
116
|
* @returns {Promise<object>} The updated session
|
|
116
117
|
*/
|
|
117
118
|
export async function startDraft(session, options = {}) {
|
|
@@ -144,6 +145,7 @@ export async function startDraft(session, options = {}) {
|
|
|
144
145
|
status: 'starting',
|
|
145
146
|
pendingModel: null,
|
|
146
147
|
...(model ? { model, agentType } : {}),
|
|
148
|
+
...(options.providerId !== undefined ? { providerId: options.providerId } : {}),
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
// Resolve skill/command invocations so skill body goes into system prompt
|
|
@@ -82,7 +82,9 @@ function buildClaudeCodeQueryParams({
|
|
|
82
82
|
abortController: controller,
|
|
83
83
|
includePartialMessages: true,
|
|
84
84
|
permissionMode: getPermissionModeForSession(session.mode),
|
|
85
|
-
|
|
85
|
+
// Match normal Claude Code CLI behavior: load user-level settings
|
|
86
|
+
// such as configured MCP servers, then project/local overrides.
|
|
87
|
+
settingSources: ['user', 'project', 'local'],
|
|
86
88
|
...(resumeSessionId && { resume: resumeSessionId }),
|
|
87
89
|
env: sessionEnv,
|
|
88
90
|
spawnClaudeCodeProcess: createClaudeCodeSpawner(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sessions, attachments, projects, kanbanBoards, kanbanLanes } from '../database.js';
|
|
2
2
|
import { DEFAULT_SERVER_PORT, DEFAULT_SYSTEM_PROMPT } from '../../../shared/src/index.js';
|
|
3
|
+
import { buildCommandButtonApiInstructions } from './commandButtonPrompts.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Get the base API URL for canvas and session operations.
|
|
@@ -422,10 +423,12 @@ This session is part of a multi-session workflow:
|
|
|
422
423
|
* @returns {string} System prompt string
|
|
423
424
|
*/
|
|
424
425
|
export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt, mode) {
|
|
426
|
+
const apiUrl = getApiBaseUrl();
|
|
425
427
|
const session = sessions.getById(sessionId);
|
|
426
428
|
const canvasWriteInstructions = buildCanvasWriteSystemPrompt(session); // Pass session object
|
|
427
429
|
const canvasReadInstructions = buildCanvasReadSystemPrompt(session); // Pass session object
|
|
428
430
|
const sessionApiInstructions = buildSessionApiInstructions(sessionId, projectId);
|
|
431
|
+
const commandButtonApiInstructions = buildCommandButtonApiInstructions(apiUrl, sessionId, projectId);
|
|
429
432
|
const kanbanApiInstructions = buildKanbanApiInstructions(sessionId, projectId);
|
|
430
433
|
const attachmentsContext = getSessionAttachmentsContext(sessionId);
|
|
431
434
|
const worktreeContext = buildWorktreeContext(session);
|
|
@@ -445,6 +448,7 @@ export function buildSystemPromptConfig(sessionId, projectId, customSystemPrompt
|
|
|
445
448
|
canvasWriteInstructions,
|
|
446
449
|
canvasReadInstructions,
|
|
447
450
|
sessionApiInstructions,
|
|
451
|
+
commandButtonApiInstructions,
|
|
448
452
|
kanbanApiInstructions
|
|
449
453
|
].filter(Boolean);
|
|
450
454
|
|
|
@@ -204,14 +204,12 @@ function stripAnthropicHostEnv(env) {
|
|
|
204
204
|
|
|
205
205
|
function replaceWithCodexCliEnv(sessionEnv, providerEnv) {
|
|
206
206
|
const target = sessionEnv;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Object.assign(
|
|
213
|
-
for (const key of Object.keys(target)) delete target[key];
|
|
214
|
-
Object.assign(target, cleaned);
|
|
207
|
+
delete target.OPENAI_API_KEY;
|
|
208
|
+
delete target.OPENAI_BASE_URL;
|
|
209
|
+
delete target.OPENAI_API_BASE;
|
|
210
|
+
delete target.OPENAI_ORG_ID;
|
|
211
|
+
delete target.OPENAI_PROJECT;
|
|
212
|
+
Object.assign(target, providerEnv);
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
function stripOpenAIBaseUrlUnlessProvided(sessionEnv, providerEnv) {
|
|
@@ -3,71 +3,102 @@ import { broadcastToSession } from '../websocket.js';
|
|
|
3
3
|
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
4
4
|
import * as summaryService from './summaryService.js';
|
|
5
5
|
import * as kanbanService from './kanbanService.js';
|
|
6
|
+
import { createVisibleFinalErrorMessage } from './visibleFinalErrorMessage.js';
|
|
6
7
|
import {
|
|
7
8
|
lastMessageIds,
|
|
8
9
|
activeSessions,
|
|
9
10
|
activeConversationIds,
|
|
11
|
+
finalErrorSessionIds,
|
|
10
12
|
associateAndBroadcastWorkLogs,
|
|
11
13
|
broadcastSessionStatus,
|
|
12
14
|
broadcastChangesUpdate,
|
|
13
15
|
} from './streamEventHandler.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
|
|
18
|
+
* Associate work logs with the last message and clean up tracking state.
|
|
18
19
|
* @param {string} sessionId
|
|
19
|
-
* @param {string} workingDirectory
|
|
20
|
-
* @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
const { handleTemplateTriggerIfNeeded, checkProactiveReschedule: _checkProactiveReschedule, handleAutoSendIfNeeded } = callbacks;
|
|
24
|
-
// Associate work logs with the last message now that the turn is complete
|
|
21
|
+
function associateAndCleanupWorkLogs(sessionId) {
|
|
25
22
|
const lastMessageId = lastMessageIds.get(sessionId);
|
|
26
23
|
if (lastMessageId) {
|
|
27
24
|
associateAndBroadcastWorkLogs(sessionId, lastMessageId);
|
|
28
25
|
lastMessageIds.delete(sessionId);
|
|
29
26
|
}
|
|
27
|
+
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Handle post-turn activities for a session that's ready for follow-up.
|
|
31
|
+
* @param {string} sessionId
|
|
32
|
+
* @param {string} workingDirectory
|
|
33
|
+
* @param {{ checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function, handleTemplateTriggerIfNeeded?: Function }} callbacks
|
|
34
|
+
* @returns {Promise<boolean>} True if rescheduled, false otherwise
|
|
35
|
+
*/
|
|
36
|
+
async function handleActiveSessionCompletion(sessionId, workingDirectory, callbacks) {
|
|
37
|
+
sessions.update(sessionId, { status: 'waiting', error: null });
|
|
38
|
+
broadcastSessionStatus(sessionId, 'waiting');
|
|
39
|
+
|
|
40
|
+
// Check if session should be proactively rescheduled based on token threshold
|
|
41
|
+
const { checkProactiveReschedule } = callbacks;
|
|
42
|
+
if (checkProactiveReschedule) {
|
|
43
|
+
const wasRescheduled = await checkProactiveReschedule(sessionId);
|
|
44
|
+
if (wasRescheduled) {
|
|
45
|
+
return true; // Session was rescheduled, don't continue with normal completion
|
|
43
46
|
}
|
|
47
|
+
}
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// Extract PR URL immediately (lightweight, no API call)
|
|
50
|
+
summaryService.extractPrUrlIfNeeded(sessionId);
|
|
51
|
+
// Trigger debounced summary generation on turn completion (not complete yet)
|
|
52
|
+
summaryService.onSessionActivity(sessionId);
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// Broadcast changes update when turn completes (real-time indicator)
|
|
55
|
+
const currentSession = sessions.getById(sessionId);
|
|
56
|
+
if (currentSession) {
|
|
57
|
+
await broadcastChangesUpdate(sessionId, currentSession.projectId, workingDirectory);
|
|
58
|
+
}
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
// Handle kanban lane movements based on targetLaneId
|
|
61
|
+
await kanbanService.handleTurnCompletion(sessionId);
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// Auto-send queued prompt if enabled (runs BEFORE template trigger)
|
|
64
|
+
const { handleAutoSendIfNeeded, handleTemplateTriggerIfNeeded } = callbacks;
|
|
65
|
+
let autoSendFired = false;
|
|
66
|
+
if (handleAutoSendIfNeeded) {
|
|
67
|
+
autoSendFired = await handleAutoSendIfNeeded(sessionId);
|
|
68
|
+
}
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
+
// Only trigger next template if auto-send did NOT fire
|
|
71
|
+
// (if auto-send fired, template will trigger after that turn completes)
|
|
72
|
+
if (!autoSendFired && handleTemplateTriggerIfNeeded) {
|
|
73
|
+
await handleTemplateTriggerIfNeeded(sessionId);
|
|
70
74
|
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle post-turn completion logic (after stream loop ends successfully)
|
|
81
|
+
* Encapsulates the duplicated completion block from runSession/continueSession/continueSessionWithExistingMessage
|
|
82
|
+
* @param {string} sessionId
|
|
83
|
+
* @param {string} workingDirectory
|
|
84
|
+
* @param {{ handleTemplateTriggerIfNeeded?: Function, checkProactiveReschedule?: Function, handleAutoSendIfNeeded?: Function }} callbacks
|
|
85
|
+
*/
|
|
86
|
+
export async function handleTurnCompletion(sessionId, workingDirectory, callbacks = {}) {
|
|
87
|
+
// Associate work logs with the last message now that the turn is complete
|
|
88
|
+
associateAndCleanupWorkLogs(sessionId);
|
|
89
|
+
|
|
90
|
+
// Sessions with final errors should not transition to waiting
|
|
91
|
+
if (finalErrorSessionIds.has(sessionId)) {
|
|
92
|
+
finalErrorSessionIds.delete(sessionId);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Session ready for follow-up - set to waiting instead of completed
|
|
97
|
+
const activeSession = activeSessions.get(sessionId);
|
|
98
|
+
if (activeSession && !activeSession.controller?.signal?.aborted) {
|
|
99
|
+
return handleActiveSessionCompletion(sessionId, workingDirectory, callbacks);
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
return false;
|
|
72
103
|
}
|
|
73
104
|
|
|
@@ -149,6 +180,7 @@ export async function handleSessionError(sessionId, error, options = {}) {
|
|
|
149
180
|
|
|
150
181
|
// Normal error handling (no reschedule or reschedule limits reached)
|
|
151
182
|
sessions.update(sessionId, { status: 'error', error: error.message });
|
|
183
|
+
createVisibleFinalErrorMessage(sessionId, error, activeConversationIds);
|
|
152
184
|
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: error.message });
|
|
153
185
|
|
|
154
186
|
// Optionally broadcast final conversation state (continueSession does this)
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
handleTextDelta as _handleTextDelta,
|
|
11
11
|
handleResultUsage,
|
|
12
12
|
} from './streamUsageHandler.js';
|
|
13
|
+
import {
|
|
14
|
+
createVisibleFinalErrorMessage,
|
|
15
|
+
normalizeFinalErrorMessage,
|
|
16
|
+
} from './visibleFinalErrorMessage.js';
|
|
13
17
|
|
|
14
18
|
// ── Shared module-level state ──────────────────────────────────────────────
|
|
15
19
|
|
|
@@ -34,6 +38,9 @@ export const currentModels = new Map();
|
|
|
34
38
|
/** @type {Map<string, Set<string>>} Track tool_use IDs that have already been logged per session */
|
|
35
39
|
export const loggedToolUseIds = new Map();
|
|
36
40
|
|
|
41
|
+
/** @type {Set<string>} Track sessions that received a final result.error event */
|
|
42
|
+
export const finalErrorSessionIds = new Set();
|
|
43
|
+
|
|
37
44
|
// ── Helper functions ───────────────────────────────────────────────────────
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -404,8 +411,11 @@ function handleResultEvent(sessionId, event) {
|
|
|
404
411
|
* @param {Object} event
|
|
405
412
|
*/
|
|
406
413
|
function handleResultError(sessionId, event) {
|
|
407
|
-
|
|
408
|
-
|
|
414
|
+
const errorMessage = normalizeFinalErrorMessage(event.error);
|
|
415
|
+
finalErrorSessionIds.add(sessionId);
|
|
416
|
+
sessions.update(sessionId, { status: 'error', error: errorMessage });
|
|
417
|
+
createVisibleFinalErrorMessage(sessionId, errorMessage, activeConversationIds);
|
|
418
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.SESSION_ERROR, { sessionId, error: errorMessage });
|
|
409
419
|
// Broadcast error status to project subscribers for session list updates
|
|
410
420
|
broadcastSessionStatus(sessionId, 'error');
|
|
411
421
|
// Extract PR URL before generating summary (PR may have been created before error)
|
|
@@ -481,6 +491,7 @@ export function cleanupSessionState(sessionId, includeConversationId = false) {
|
|
|
481
491
|
thinkingAccumulators.delete(sessionId);
|
|
482
492
|
currentModels.delete(sessionId);
|
|
483
493
|
loggedToolUseIds.delete(sessionId);
|
|
494
|
+
finalErrorSessionIds.delete(sessionId);
|
|
484
495
|
activeSessions.delete(sessionId);
|
|
485
496
|
if (includeConversationId) {
|
|
486
497
|
activeConversationIds.delete(sessionId);
|
|
@@ -74,6 +74,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
|
|
|
74
74
|
const turnData = currentTurnUsage.get(conversationId) || {
|
|
75
75
|
inputTokens: 0,
|
|
76
76
|
outputTokens: 0,
|
|
77
|
+
thinkingTokens: 0,
|
|
77
78
|
lastMessageOutput: 0,
|
|
78
79
|
cacheReadInputTokens: 0,
|
|
79
80
|
cacheCreationInputTokens: 0,
|
|
@@ -83,6 +84,7 @@ export function handleTextDelta(sessionId, delta, textAccumulators) {
|
|
|
83
84
|
const broadcastUsage = {
|
|
84
85
|
inputTokens: turnData.inputTokens,
|
|
85
86
|
outputTokens: turnData.outputTokens + Math.max(turnData.lastMessageOutput, newEstimate),
|
|
87
|
+
thinkingTokens: turnData.thinkingTokens,
|
|
86
88
|
cacheReadInputTokens: turnData.cacheReadInputTokens,
|
|
87
89
|
cacheCreationInputTokens: turnData.cacheCreationInputTokens,
|
|
88
90
|
};
|
|
@@ -123,6 +125,7 @@ export function extractTurnUsage(sessionId, event) {
|
|
|
123
125
|
return {
|
|
124
126
|
inputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'inputTokens', snake: 'input_tokens' }),
|
|
125
127
|
outputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'outputTokens', snake: 'output_tokens' }),
|
|
128
|
+
thinkingTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'thinkingTokens', snake: 'thinking_tokens' }),
|
|
126
129
|
cacheReadInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheReadInputTokens', snake: 'cache_read_input_tokens' }),
|
|
127
130
|
cacheCreationInputTokens: resolveTokenField(modelUsageEntry, event.usage, { camel: 'cacheCreationInputTokens', snake: 'cache_creation_input_tokens' }),
|
|
128
131
|
webSearchRequests: modelUsageEntry?.webSearchRequests || 0,
|
|
@@ -142,6 +145,7 @@ export function buildCumulativeSessionUsage(sessionId, turnUsage) {
|
|
|
142
145
|
return {
|
|
143
146
|
inputTokens: (currentSession.inputTokens || 0) + turnUsage.inputTokens,
|
|
144
147
|
outputTokens: (currentSession.outputTokens || 0) + turnUsage.outputTokens,
|
|
148
|
+
thinkingTokens: (currentSession.thinkingTokens || 0) + turnUsage.thinkingTokens,
|
|
145
149
|
cacheReadInputTokens: (currentSession.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
|
|
146
150
|
cacheCreationInputTokens: (currentSession.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
|
|
147
151
|
webSearchRequests: (currentSession.webSearchRequests || 0) + turnUsage.webSearchRequests,
|
|
@@ -162,6 +166,7 @@ export function updateConversationUsage(conversationId, currentConversation, tur
|
|
|
162
166
|
const cumulativeConversationUsage = {
|
|
163
167
|
inputTokens: (currentConversation.inputTokens || 0) + turnUsage.inputTokens,
|
|
164
168
|
outputTokens: (currentConversation.outputTokens || 0) + turnUsage.outputTokens,
|
|
169
|
+
thinkingTokens: (currentConversation.thinkingTokens || 0) + turnUsage.thinkingTokens,
|
|
165
170
|
cacheReadInputTokens: (currentConversation.cacheReadInputTokens || 0) + turnUsage.cacheReadInputTokens,
|
|
166
171
|
cacheCreationInputTokens: (currentConversation.cacheCreationInputTokens || 0) + turnUsage.cacheCreationInputTokens,
|
|
167
172
|
webSearchRequests: (currentConversation.webSearchRequests || 0) + turnUsage.webSearchRequests,
|
|
@@ -197,6 +202,7 @@ export function handleResultUsage(sessionId, event) {
|
|
|
197
202
|
usage: updatedConversation ? {
|
|
198
203
|
inputTokens: updatedConversation.inputTokens,
|
|
199
204
|
outputTokens: updatedConversation.outputTokens,
|
|
205
|
+
thinkingTokens: updatedConversation.thinkingTokens,
|
|
200
206
|
cacheReadInputTokens: updatedConversation.cacheReadInputTokens,
|
|
201
207
|
cacheCreationInputTokens: updatedConversation.cacheCreationInputTokens,
|
|
202
208
|
webSearchRequests: updatedConversation.webSearchRequests,
|
|
@@ -49,11 +49,16 @@ function logResultUsage(callId, event) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Build the query parameters for the Claude SDK call.
|
|
51
51
|
* @param {string} prompt - The prompt to send
|
|
52
|
-
* @param {{ systemPrompt?: string, jsonSchema?: Object }} options
|
|
52
|
+
* @param {{ systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object }} options
|
|
53
53
|
* @returns {Object} queryParams ready for the SDK query function
|
|
54
54
|
*/
|
|
55
55
|
function buildClaudeRequest(prompt, options) {
|
|
56
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
systemPrompt = null,
|
|
58
|
+
jsonSchema = null,
|
|
59
|
+
model = 'claude-haiku-4-5-20251001',
|
|
60
|
+
env = null,
|
|
61
|
+
} = options || {};
|
|
57
62
|
const schema = jsonSchema || SESSION_SUMMARY_SCHEMA;
|
|
58
63
|
|
|
59
64
|
return {
|
|
@@ -62,7 +67,8 @@ function buildClaudeRequest(prompt, options) {
|
|
|
62
67
|
cwd: process.cwd(),
|
|
63
68
|
permissionMode: 'bypassPermissions',
|
|
64
69
|
maxTurns: 1,
|
|
65
|
-
model
|
|
70
|
+
model,
|
|
71
|
+
...(env && { env }),
|
|
66
72
|
...(systemPrompt && { systemPrompt }),
|
|
67
73
|
outputFormat: {
|
|
68
74
|
type: 'json_schema',
|
|
@@ -108,6 +114,21 @@ async function handleClaudeResponse(eventStream, callId) {
|
|
|
108
114
|
return state.responseText;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
function buildSummaryCallMetadata({ logMeta, model, providerId, selectionReason, promptLength }) {
|
|
118
|
+
return {
|
|
119
|
+
sessionId: logMeta.sessionId,
|
|
120
|
+
conversationId: logMeta.conversationId || null,
|
|
121
|
+
agentType: 'summary',
|
|
122
|
+
model,
|
|
123
|
+
callType: logMeta.callType,
|
|
124
|
+
promptLength,
|
|
125
|
+
metadata: {
|
|
126
|
+
...(providerId ? { providerId } : {}),
|
|
127
|
+
...(selectionReason ? { selectionReason } : {}),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
export const SESSION_SUMMARY_SCHEMA = {
|
|
112
133
|
type: 'object',
|
|
113
134
|
properties: {
|
|
@@ -130,11 +151,16 @@ export const SESSION_SUMMARY_SCHEMA = {
|
|
|
130
151
|
* @param {string} prompt - The prompt to send
|
|
131
152
|
* @param {Array} recentMessages - Messages (for mock mode context)
|
|
132
153
|
* @param {string} sessionStatus - Session status (for mock mode context)
|
|
133
|
-
* @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object }} options - Optional parameters
|
|
154
|
+
* @param {{ logMeta?: Object, systemPrompt?: string, jsonSchema?: Object, model?: string, env?: Object, providerId?: string|null, selectionReason?: string }} options - Optional parameters
|
|
134
155
|
* @returns {Promise<string>} The text response (JSON string)
|
|
135
156
|
*/
|
|
136
157
|
export async function callClaude(prompt, recentMessages, sessionStatus, options = {}) {
|
|
137
|
-
const {
|
|
158
|
+
const {
|
|
159
|
+
logMeta = null,
|
|
160
|
+
model = 'claude-haiku-4-5-20251001',
|
|
161
|
+
providerId = null,
|
|
162
|
+
selectionReason = null,
|
|
163
|
+
} = options || {};
|
|
138
164
|
// Build stable key for VCR cassette (session prompts are hardcoded strings in E2E tests)
|
|
139
165
|
let keyHint = null;
|
|
140
166
|
if (process.env.VCR_MODE && logMeta?.sessionId) {
|
|
@@ -151,14 +177,13 @@ export async function callClaude(prompt, recentMessages, sessionStatus, options
|
|
|
151
177
|
// Start logging if metadata provided
|
|
152
178
|
let callId = null;
|
|
153
179
|
if (logMeta) {
|
|
154
|
-
callId = agentCallLogger.startCall({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
callType: logMeta.callType,
|
|
180
|
+
callId = agentCallLogger.startCall(buildSummaryCallMetadata({
|
|
181
|
+
logMeta,
|
|
182
|
+
model,
|
|
183
|
+
providerId,
|
|
184
|
+
selectionReason,
|
|
160
185
|
promptLength: prompt.length,
|
|
161
|
-
});
|
|
186
|
+
}));
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
try {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { callClaude, SESSION_SUMMARY_SCHEMA } from './summaryClaudeClient.js';
|
|
3
|
+
import { agentCallLogger } from './agentCallLogger.js';
|
|
4
|
+
import { buildProviderEnv } from './sessionProvider.js';
|
|
5
|
+
import { resolveSummaryModel } from './summaryModelResolver.js';
|
|
6
|
+
|
|
7
|
+
export { SESSION_SUMMARY_SCHEMA };
|
|
8
|
+
|
|
9
|
+
export async function callSummaryModel(prompt, recentMessages, sessionStatus, options = {}) {
|
|
10
|
+
const resolution = options.resolvedModel || resolveSummaryModel(options.summarySettings || {});
|
|
11
|
+
if (resolution.kind === 'openai') {
|
|
12
|
+
return callOpenAISummaryModel(prompt, resolution, options);
|
|
13
|
+
}
|
|
14
|
+
return callAnthropicSummaryModel({ prompt, recentMessages, sessionStatus, resolution, options });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function callAnthropicSummaryModel({ prompt, recentMessages, sessionStatus, resolution, options }) {
|
|
18
|
+
const providerEnv = resolution.provider && !resolution.provider.isBuiltIn
|
|
19
|
+
? { ...process.env, ...buildProviderEnv(resolution.provider) }
|
|
20
|
+
: null;
|
|
21
|
+
|
|
22
|
+
return callClaude(prompt, recentMessages, sessionStatus, {
|
|
23
|
+
...options,
|
|
24
|
+
model: resolution.model,
|
|
25
|
+
providerId: resolution.providerId,
|
|
26
|
+
selectionReason: resolution.selectionReason,
|
|
27
|
+
...(providerEnv ? { env: providerEnv } : {}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callOpenAISummaryModel(prompt, resolution, options) {
|
|
32
|
+
const { logMeta = null, systemPrompt = null, jsonSchema = null } = options || {};
|
|
33
|
+
const schema = jsonSchema || SESSION_SUMMARY_SCHEMA;
|
|
34
|
+
const provider = resolution.provider;
|
|
35
|
+
const client = createOpenAIClient(provider);
|
|
36
|
+
|
|
37
|
+
const callId = startOpenAISummaryLog(logMeta, resolution, prompt.length);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await createOpenAIChatCompletion(client, {
|
|
41
|
+
model: resolution.model,
|
|
42
|
+
prompt,
|
|
43
|
+
systemPrompt,
|
|
44
|
+
schema,
|
|
45
|
+
structured: true,
|
|
46
|
+
}).catch(async (error) => {
|
|
47
|
+
if (!isStructuredOutputUnsupported(error)) throw error;
|
|
48
|
+
return createOpenAIChatCompletion(client, {
|
|
49
|
+
model: resolution.model,
|
|
50
|
+
prompt: withJsonOnlyInstruction(prompt, schema),
|
|
51
|
+
systemPrompt,
|
|
52
|
+
schema,
|
|
53
|
+
structured: false,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
logOpenAIUsage(callId, response.usage);
|
|
58
|
+
|
|
59
|
+
if (callId) {
|
|
60
|
+
agentCallLogger.completeCall(callId, { success: true });
|
|
61
|
+
}
|
|
62
|
+
return extractOpenAIContent(response);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (callId) {
|
|
65
|
+
agentCallLogger.completeCall(callId, { success: false, error });
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startOpenAISummaryLog(logMeta, resolution, promptLength) {
|
|
72
|
+
if (!logMeta) return null;
|
|
73
|
+
return agentCallLogger.startCall({
|
|
74
|
+
sessionId: logMeta.sessionId,
|
|
75
|
+
conversationId: logMeta.conversationId || null,
|
|
76
|
+
agentType: 'summary',
|
|
77
|
+
model: resolution.model,
|
|
78
|
+
callType: logMeta.callType,
|
|
79
|
+
promptLength,
|
|
80
|
+
metadata: {
|
|
81
|
+
...(resolution.providerId ? { providerId: resolution.providerId } : {}),
|
|
82
|
+
...(resolution.selectionReason ? { selectionReason: resolution.selectionReason } : {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function logOpenAIUsage(callId, usage) {
|
|
88
|
+
if (!callId || !usage) return;
|
|
89
|
+
agentCallLogger.updateUsage(callId, {
|
|
90
|
+
inputTokens: usage.prompt_tokens || 0,
|
|
91
|
+
outputTokens: usage.completion_tokens || 0,
|
|
92
|
+
thinkingTokens: 0,
|
|
93
|
+
cacheReadInputTokens: 0,
|
|
94
|
+
cacheCreationInputTokens: 0,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createOpenAIClient(provider) {
|
|
99
|
+
const options = {
|
|
100
|
+
apiKey: provider?.authToken || process.env.OPENAI_API_KEY || 'missing',
|
|
101
|
+
};
|
|
102
|
+
if (provider?.baseUrl) options.baseURL = provider.baseUrl;
|
|
103
|
+
if (provider?.apiTimeoutMs) options.timeout = provider.apiTimeoutMs;
|
|
104
|
+
return new OpenAI(options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createOpenAIChatCompletion(client, { model, prompt, systemPrompt, schema, structured }) {
|
|
108
|
+
const messages = [];
|
|
109
|
+
if (systemPrompt) messages.push({ role: 'system', content: systemPrompt });
|
|
110
|
+
messages.push({ role: 'user', content: prompt });
|
|
111
|
+
|
|
112
|
+
const request = {
|
|
113
|
+
model,
|
|
114
|
+
messages,
|
|
115
|
+
temperature: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (structured) {
|
|
119
|
+
request.response_format = {
|
|
120
|
+
type: 'json_schema',
|
|
121
|
+
json_schema: {
|
|
122
|
+
name: 'session_summary',
|
|
123
|
+
schema,
|
|
124
|
+
strict: false,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
request.response_format = { type: 'json_object' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return client.chat.completions.create(request);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractOpenAIContent(response) {
|
|
135
|
+
const content = response?.choices?.[0]?.message?.content;
|
|
136
|
+
if (Array.isArray(content)) {
|
|
137
|
+
return content
|
|
138
|
+
.map((part) => part?.text || part?.content || '')
|
|
139
|
+
.join('');
|
|
140
|
+
}
|
|
141
|
+
return content || '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isStructuredOutputUnsupported(error) {
|
|
145
|
+
const message = `${error?.message || ''} ${error?.code || ''} ${error?.type || ''}`.toLowerCase();
|
|
146
|
+
return [400, 404, 422].includes(error?.status)
|
|
147
|
+
|| message.includes('response_format')
|
|
148
|
+
|| message.includes('json_schema')
|
|
149
|
+
|| message.includes('unsupported');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function withJsonOnlyInstruction(prompt, schema) {
|
|
153
|
+
return `${prompt}\n\nReturn only a valid JSON object matching this JSON Schema:\n${JSON.stringify(schema)}`;
|
|
154
|
+
}
|