mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat(queue): implement queue scheduler service and API routes
- Added QueueSchedulerService to manage task queue lifecycle, including state machine, dependency resolution, and session management. - Implemented HTTP API endpoints for queue scheduling: - POST /api/queue/execute: Submit items to the scheduler. - GET /api/queue/scheduler/state: Retrieve full scheduler state. - POST /api/queue/scheduler/start: Start scheduling loop with items. - POST /api/queue/scheduler/pause: Pause scheduling. - POST /api/queue/scheduler/stop: Graceful stop of the scheduler. - POST /api/queue/scheduler/config: Update scheduler configuration. - Introduced types for queue items, scheduler state, and WebSocket messages to ensure type safety and compatibility with the backend. - Added static model lists for LiteLLM as a fallback for available models.
This commit is contained in:
@@ -390,9 +390,13 @@ export function generateTransactionId(conversationId: string): TransactionId {
|
||||
* Inject transaction ID into user prompt
|
||||
* @param prompt - Original user prompt
|
||||
* @param txId - Transaction ID to inject
|
||||
* @returns Prompt with transaction ID injected at the start
|
||||
* @returns Prompt with transaction ID injected at the start, or empty string if prompt is empty
|
||||
*/
|
||||
export function injectTransactionId(prompt: string, txId: TransactionId): string {
|
||||
// Don't inject TX ID for empty prompts (e.g., review mode with target flags)
|
||||
if (!prompt || !prompt.trim()) {
|
||||
return '';
|
||||
}
|
||||
return `[CCW-TX-ID: ${txId}]\n\n${prompt}`;
|
||||
}
|
||||
|
||||
@@ -844,8 +848,15 @@ async function executeCliTool(
|
||||
|
||||
// Inject transaction ID at the start of the final prompt for session tracking
|
||||
// This enables exact session matching during parallel execution scenarios
|
||||
finalPrompt = injectTransactionId(finalPrompt, transactionId);
|
||||
debugLog('TX_ID', `Injected transaction ID into prompt`, { transactionId, promptLength: finalPrompt.length });
|
||||
// Skip injection for review mode with target flags (uncommitted/base/commit) as these
|
||||
// modes don't accept prompt arguments in codex CLI
|
||||
const isReviewWithTarget = mode === 'review' && (uncommitted || base || commit);
|
||||
if (!isReviewWithTarget) {
|
||||
finalPrompt = injectTransactionId(finalPrompt, transactionId);
|
||||
debugLog('TX_ID', `Injected transaction ID into prompt`, { transactionId, promptLength: finalPrompt.length });
|
||||
} else {
|
||||
debugLog('TX_ID', `Skipped transaction ID injection for review mode with target flag`);
|
||||
}
|
||||
|
||||
// Check tool availability
|
||||
const toolStatus = await checkToolAvailability(tool);
|
||||
|
||||
@@ -502,9 +502,11 @@ export class CliHistoryStore {
|
||||
* Save or update a conversation
|
||||
*/
|
||||
saveConversation(conversation: ConversationRecord): void {
|
||||
const promptPreview = conversation.turns.length > 0
|
||||
? conversation.turns[conversation.turns.length - 1].prompt.substring(0, 100)
|
||||
: '';
|
||||
// Ensure prompt is a string before calling substring
|
||||
const lastTurn = conversation.turns.length > 0 ? conversation.turns[conversation.turns.length - 1] : null;
|
||||
const rawPrompt = lastTurn?.prompt ?? '';
|
||||
const promptStr = typeof rawPrompt === 'string' ? rawPrompt : JSON.stringify(rawPrompt);
|
||||
const promptPreview = promptStr.substring(0, 100);
|
||||
|
||||
const upsertConversation = this.db.prepare(`
|
||||
INSERT INTO conversations (id, created_at, updated_at, tool, model, mode, category, total_duration_ms, turn_count, latest_status, prompt_preview, parent_execution_id, project_root, relative_path)
|
||||
@@ -609,7 +611,8 @@ export class CliHistoryStore {
|
||||
turns: turns.map(t => ({
|
||||
turn: t.turn_number,
|
||||
timestamp: t.timestamp,
|
||||
prompt: t.prompt,
|
||||
// Ensure prompt is always a string (handle legacy object data)
|
||||
prompt: typeof t.prompt === 'string' ? t.prompt : JSON.stringify(t.prompt),
|
||||
duration_ms: t.duration_ms,
|
||||
status: t.status,
|
||||
exit_code: t.exit_code,
|
||||
@@ -840,7 +843,10 @@ export class CliHistoryStore {
|
||||
category: r.category || 'user',
|
||||
duration_ms: r.total_duration_ms,
|
||||
turn_count: r.turn_count,
|
||||
prompt_preview: r.prompt_preview || ''
|
||||
// Ensure prompt_preview is always a string (handle legacy object data)
|
||||
prompt_preview: typeof r.prompt_preview === 'string'
|
||||
? r.prompt_preview
|
||||
: (r.prompt_preview ? JSON.stringify(r.prompt_preview) : '')
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1197,3 +1197,30 @@ export function getToolSessionPath(tool: string): string | null {
|
||||
const discoverer = discoverers[tool];
|
||||
return discoverer?.basePath || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all native sessions from all supported CLI tools
|
||||
* Aggregates sessions from Gemini, Qwen, Codex, Claude, and OpenCode
|
||||
* @param options - Optional filtering (workingDir, limit, afterTimestamp)
|
||||
* @returns Combined sessions sorted by updatedAt descending
|
||||
*/
|
||||
export function listAllNativeSessions(options?: SessionDiscoveryOptions): NativeSession[] {
|
||||
const allSessions: NativeSession[] = [];
|
||||
|
||||
// Collect sessions from all discoverers
|
||||
for (const tool of Object.keys(discoverers)) {
|
||||
const discoverer = discoverers[tool];
|
||||
const sessions = discoverer.getSessions(options);
|
||||
allSessions.push(...sessions);
|
||||
}
|
||||
|
||||
// Sort by updatedAt descending
|
||||
allSessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
||||
|
||||
// Apply limit if provided
|
||||
if (options?.limit) {
|
||||
return allSessions.slice(0, options.limit);
|
||||
}
|
||||
|
||||
return allSessions;
|
||||
}
|
||||
|
||||
@@ -236,13 +236,18 @@ function parseGeminiQwenSession(content: string, tool: string): ParsedSession {
|
||||
let model: string | undefined;
|
||||
|
||||
for (const msg of session.messages) {
|
||||
// Ensure content is always a string (handle legacy object data like {text: "..."})
|
||||
const contentStr = typeof msg.content === 'string'
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content);
|
||||
|
||||
if (msg.type === 'user') {
|
||||
turnNumber++;
|
||||
turns.push({
|
||||
turnNumber,
|
||||
timestamp: msg.timestamp,
|
||||
role: 'user',
|
||||
content: msg.content
|
||||
content: contentStr
|
||||
});
|
||||
} else if (msg.type === 'gemini' || msg.type === 'qwen') {
|
||||
// Find the corresponding user turn
|
||||
@@ -255,7 +260,7 @@ function parseGeminiQwenSession(content: string, tool: string): ParsedSession {
|
||||
turnNumber,
|
||||
timestamp: msg.timestamp,
|
||||
role: 'assistant',
|
||||
content: msg.content,
|
||||
content: contentStr,
|
||||
thoughts: thoughts.length > 0 ? thoughts : undefined,
|
||||
tokens: msg.tokens ? {
|
||||
input: msg.tokens.input,
|
||||
@@ -428,7 +433,11 @@ function parseCodexSession(content: string): ParsedSession {
|
||||
currentTurn++;
|
||||
const textContent = item.payload.content
|
||||
?.filter(c => c.type === 'input_text')
|
||||
.map(c => c.text)
|
||||
.map(c => {
|
||||
// Ensure text is a string (handle legacy object data like {text: "..."})
|
||||
const txt = c.text;
|
||||
return typeof txt === 'string' ? txt : JSON.stringify(txt);
|
||||
})
|
||||
.join('\n') || '';
|
||||
|
||||
turns.push({
|
||||
@@ -461,7 +470,11 @@ function parseCodexSession(content: string): ParsedSession {
|
||||
// Assistant message (final response)
|
||||
const textContent = item.payload.content
|
||||
?.filter(c => c.type === 'output_text' || c.type === 'text')
|
||||
.map(c => c.text)
|
||||
.map(c => {
|
||||
// Ensure text is a string (handle legacy object data like {text: "..."})
|
||||
const txt = c.text;
|
||||
return typeof txt === 'string' ? txt : JSON.stringify(txt);
|
||||
})
|
||||
.join('\n') || '';
|
||||
|
||||
if (textContent) {
|
||||
|
||||
Reference in New Issue
Block a user