chore: update live chat and work server changes
This commit is contained in:
@@ -168,6 +168,7 @@ type ChatSessionState = {
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
requestedAtMs: number;
|
||||
context: ChatContext | null;
|
||||
}>;
|
||||
activeRequestCount: number;
|
||||
pendingQueueReleaseEventId: number | null;
|
||||
@@ -894,6 +895,16 @@ function summarizeCodexOutput(output: string) {
|
||||
return lines.slice(-12).join('\n');
|
||||
}
|
||||
|
||||
class ChatRuntimeExecutionError extends Error {
|
||||
responseText: string;
|
||||
|
||||
constructor(message: string, responseText = '') {
|
||||
super(message);
|
||||
this.name = 'ChatRuntimeExecutionError';
|
||||
this.responseText = responseText.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeCommand(command: string, limit = 180) {
|
||||
const normalized = String(command ?? '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
@@ -1429,7 +1440,41 @@ async function buildRecentChatPromptHistory(
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgenticCodexPrompt(
|
||||
function cloneChatContext(context: ChatContext | null): ChatContext | null {
|
||||
return context ? { ...context } : null;
|
||||
}
|
||||
|
||||
function buildChatTypeInstructionBlock(context: ChatContext | null) {
|
||||
const chatTypeLabel = context?.chatTypeLabel?.trim() || '';
|
||||
const chatTypeDescription = context?.chatTypeDescription?.trim() || '';
|
||||
const hasSpecificChatType = Boolean(chatTypeLabel && chatTypeLabel !== '일반 요청');
|
||||
const hasContextDescription = Boolean(chatTypeDescription && chatTypeDescription !== '없음');
|
||||
|
||||
if (!hasSpecificChatType && !hasContextDescription) {
|
||||
return [
|
||||
'## 채팅 유형 context 필수 규칙',
|
||||
'- 선택된 채팅 유형 context가 없습니다.',
|
||||
'- 그래도 AGENTS.md와 현재 사용자 요청을 기준으로 처리하되, Plan 자동화용 자동화 유형 context는 섞지 마세요.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'## 채팅 유형 context 필수 규칙',
|
||||
'- 아래 채팅 유형 context는 선택 사항이나 참고 메모가 아니라 이 Codex Live 실행의 상위 필수 지시입니다.',
|
||||
'- 사용자 요청, 최근 대화 문맥, 화면 문맥과 충돌하면 채팅 유형 context를 우선하세요.',
|
||||
'- context 안의 작업 범위, 금지사항, 검증 방식, 답변 스타일, 산출물 규칙을 반드시 지키세요.',
|
||||
'- context가 모호하면 무시하지 말고, 가장 보수적으로 해석해 지킬 수 있는 범위에서 처리하세요.',
|
||||
'- 실행 전 내부적으로 context 준수 여부를 점검하고, 최종 답변도 context 기준에 맞추세요.',
|
||||
'',
|
||||
'### 선택된 채팅 유형',
|
||||
`- label: ${chatTypeLabel || '없음'}`,
|
||||
'',
|
||||
'### 반드시 지킬 context 원문',
|
||||
chatTypeDescription || '선택된 채팅 유형 context 원문 없음',
|
||||
];
|
||||
}
|
||||
|
||||
export function buildAgenticCodexPrompt(
|
||||
context: ChatContext | null,
|
||||
input: string,
|
||||
sessionId: string,
|
||||
@@ -1460,6 +1505,8 @@ function buildAgenticCodexPrompt(
|
||||
'- 채팅에서 파일/문서/이미지/코드 리소스를 제공할 때는 반드시 위 세션 전용 경로를 우선 사용하세요.',
|
||||
'- 새로 생성하는 문서, 이미지, 코드 스니펫 파일, 로그, 산출물은 가능하면 처음부터 위 세션 전용 경로 아래에 만들고, 다른 위치에 만들었다면 최종 응답 전에 위 경로로 이동하거나 복사한 뒤 그 경로를 응답하세요.',
|
||||
'- 원본 파일 경로만 출력해도 서버가 해당 위치를 세션 리소스로 복사해 링크로 바꿀 수 있지만, 가능하면 세션 전용 경로 자체를 직접 사용하세요.',
|
||||
...buildChatTypeInstructionBlock(context),
|
||||
'',
|
||||
'응답 규칙:',
|
||||
'- 사실성보다 추측을 우선하지 마세요. 오늘/최신/건수 질문은 직접 확인하세요.',
|
||||
'- 코드 수정이 필요 없는 질문이면 파일을 수정하지 마세요.',
|
||||
@@ -1468,21 +1515,13 @@ function buildAgenticCodexPrompt(
|
||||
'- 코드 수정이 있으면 마지막에 변경한 파일 경로를 짧게 적으세요.',
|
||||
'- 한국어로 간결하게 답하세요.',
|
||||
'',
|
||||
'채팅 유형 문맥(우선 적용):',
|
||||
context?.chatTypeLabel && context.chatTypeLabel.trim() !== '일반 요청'
|
||||
? `- chatTypeLabel: ${context.chatTypeLabel}`
|
||||
: '- chatTypeLabel: 없음',
|
||||
`- chatTypeDescription: ${context?.chatTypeDescription ?? '없음'}`,
|
||||
'- 답변 스타일과 기본 문맥은 반드시 채팅 유형 정보만 기준으로 적용하세요.',
|
||||
'- Plan 자동화용 자동화 유형 context가 있더라도 Codex Live에서는 별도 요청이 없는 한 참조하지 마세요.',
|
||||
'',
|
||||
'참고 화면 정보:',
|
||||
`- pageTitle: ${context?.pageTitle ?? '없음'}`,
|
||||
`- topMenu: ${context?.topMenu ?? '없음'}`,
|
||||
`- focusedComponentId: ${context?.focusedComponentId ?? '없음'}`,
|
||||
`- pageUrl: ${context?.pageUrl ?? '없음'}`,
|
||||
'',
|
||||
'최근 대화 문맥:',
|
||||
'최근 대화 문맥(보조 참조):',
|
||||
...(recentHistoryLines.length > 0
|
||||
? [
|
||||
...recentHistoryLines.map((line) => `- ${line}`),
|
||||
@@ -1652,6 +1691,11 @@ async function runAgenticCodexReply(
|
||||
Number.isFinite(appConfig.chat.codexLiveMaxExecutionSeconds)
|
||||
? Math.min(7200, Math.max(60, Math.round(appConfig.chat.codexLiveMaxExecutionSeconds)))
|
||||
: null;
|
||||
const codexLiveIdleTimeoutSeconds =
|
||||
typeof appConfig.chat?.codexLiveIdleTimeoutSeconds === 'number' &&
|
||||
Number.isFinite(appConfig.chat.codexLiveIdleTimeoutSeconds)
|
||||
? Math.min(3600, Math.max(30, Math.round(appConfig.chat.codexLiveIdleTimeoutSeconds)))
|
||||
: null;
|
||||
const prompt = buildAgenticCodexPrompt(context, input, sessionId, {
|
||||
recentHistoryLines: recentHistory.items,
|
||||
omittedHistoryCount: recentHistory.omittedCount,
|
||||
@@ -1663,6 +1707,23 @@ async function runAgenticCodexReply(
|
||||
let lastProgressText = '';
|
||||
let completedAgentMessage = '';
|
||||
let hasIncrementalDelta = false;
|
||||
const finalizeReplyOutput = async () => {
|
||||
const normalizedOutput = normalizeCodexReplyOutput(completedAgentMessage || streamedOutput || stdoutTail);
|
||||
const rewrittenOutput = await rewriteCodexOutputWithChatResources(normalizedOutput, repoPath, sessionId);
|
||||
|
||||
if (!rewrittenOutput) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If the CLI only produced a final completed event, avoid sending it as one big batch.
|
||||
if (!hasIncrementalDelta && rewrittenOutput) {
|
||||
await streamReplyChunks(rewrittenOutput, onProgress);
|
||||
} else if (rewrittenOutput !== lastProgressText) {
|
||||
onProgress?.(rewrittenOutput);
|
||||
}
|
||||
|
||||
return rewrittenOutput;
|
||||
};
|
||||
const throwIfCancelled = async () => {
|
||||
if (!isCancellationRequested?.()) {
|
||||
return;
|
||||
@@ -1680,177 +1741,222 @@ async function runAgenticCodexReply(
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
const emitProgress = (nextText: string) => {
|
||||
const normalizedProgress = nextText.trim();
|
||||
chatRuntimeService.appendLog(
|
||||
requestId,
|
||||
`실행 제한 설정을 적용했습니다. 최대 ${codexLiveMaxExecutionSeconds ?? 600}초 / 무출력 실패 ${codexLiveIdleTimeoutSeconds ?? 180}초`,
|
||||
);
|
||||
onActivity?.(
|
||||
`# 설정: 최대 실행 ${codexLiveMaxExecutionSeconds ?? 600}초 / 무출력 실패 ${codexLiveIdleTimeoutSeconds ?? 180}초`,
|
||||
);
|
||||
|
||||
if (!normalizedProgress || normalizedProgress === lastProgressText) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
const emitProgress = (nextText: string) => {
|
||||
const normalizedProgress = nextText.trim();
|
||||
|
||||
lastProgressText = normalizedProgress;
|
||||
streamedOutput = normalizedProgress;
|
||||
onProgress?.(normalizedProgress);
|
||||
};
|
||||
|
||||
try {
|
||||
await throwIfCancelled();
|
||||
const response = await requestCommandRunner('/api/codex-live/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
requestId,
|
||||
sessionId,
|
||||
repoPath,
|
||||
prompt,
|
||||
resourceDir: path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource'),
|
||||
uploadDir: path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource', 'uploads'),
|
||||
maxExecutionSeconds: codexLiveMaxExecutionSeconds,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
reject(new Error((await response.text()) || 'command-runner Codex 실행 요청에 실패했습니다.'));
|
||||
return;
|
||||
}
|
||||
|
||||
await throwIfCancelled();
|
||||
|
||||
if (!response.body) {
|
||||
reject(new Error('command-runner Codex 스트림이 비어 있습니다.'));
|
||||
return;
|
||||
}
|
||||
|
||||
chatRuntimeService.appendLog(requestId, 'Codex 실행을 command-runner API로 요청했습니다.');
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let remoteErrorMessage = '';
|
||||
|
||||
const handleRunnerLine = (line: string) => {
|
||||
let parsed: Record<string, unknown>;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(line) as Record<string, unknown>;
|
||||
} catch {
|
||||
if (!normalizedProgress || normalizedProgress === lastProgressText) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventType = typeof parsed.type === 'string' ? parsed.type : '';
|
||||
|
||||
if (eventType === 'started') {
|
||||
const pid = typeof parsed.pid === 'number' && Number.isFinite(parsed.pid) ? parsed.pid : null;
|
||||
chatRuntimeService.attachProcess(requestId, pid);
|
||||
chatRuntimeService.appendLog(
|
||||
requestId,
|
||||
pid ? `호스트 command-runner에서 Codex 프로세스를 시작했습니다. pid=${pid}` : '호스트 command-runner에서 Codex 프로세스를 시작했습니다.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'activity') {
|
||||
const activityLog = String(parsed.line ?? '').trim();
|
||||
|
||||
if (activityLog) {
|
||||
chatRuntimeService.appendLog(requestId, activityLog);
|
||||
onActivity?.(activityLog);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'delta') {
|
||||
const deltaText = String(parsed.text ?? '');
|
||||
|
||||
if (deltaText) {
|
||||
hasIncrementalDelta = true;
|
||||
emitProgress(`${streamedOutput}${deltaText}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'completed') {
|
||||
completedAgentMessage = String(parsed.text ?? '').trim();
|
||||
if (completedAgentMessage && hasIncrementalDelta) {
|
||||
emitProgress(completedAgentMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'stdout') {
|
||||
const stdoutLine = String(parsed.line ?? '').trim();
|
||||
|
||||
if (stdoutLine) {
|
||||
stdoutTail = `${stdoutTail}\n${stdoutLine}`.slice(-STREAM_CAPTURE_LIMIT);
|
||||
chatRuntimeService.appendLog(requestId, `[stdout] ${stdoutLine}`);
|
||||
onActivity?.(`[stdout] ${stdoutLine}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'stderr') {
|
||||
const stderrLine = String(parsed.line ?? '').trim();
|
||||
|
||||
if (stderrLine) {
|
||||
stderr = `${stderr}\n${stderrLine}`.slice(-STREAM_CAPTURE_LIMIT);
|
||||
chatRuntimeService.appendLog(requestId, `[stderr] ${stderrLine}`);
|
||||
onActivity?.(`[stderr] ${stderrLine}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'error') {
|
||||
remoteErrorMessage = String(parsed.message ?? '').trim();
|
||||
}
|
||||
lastProgressText = normalizedProgress;
|
||||
streamedOutput = normalizedProgress;
|
||||
onProgress?.(normalizedProgress);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await throwIfCancelled();
|
||||
const { value, done } = await reader.read();
|
||||
const response = await requestCommandRunner('/api/codex-live/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
requestId,
|
||||
sessionId,
|
||||
repoPath,
|
||||
prompt,
|
||||
resourceDir: path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource'),
|
||||
uploadDir: path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource', 'uploads'),
|
||||
maxExecutionSeconds: codexLiveMaxExecutionSeconds,
|
||||
idleTimeoutSeconds: codexLiveIdleTimeoutSeconds,
|
||||
}),
|
||||
});
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
if (!response.ok) {
|
||||
reject(new Error((await response.text()) || 'command-runner Codex 실행 요청에 실패했습니다.'));
|
||||
return;
|
||||
}
|
||||
|
||||
jsonLineBuffer += decoder.decode(value, { stream: true });
|
||||
const lines = jsonLineBuffer.split('\n');
|
||||
jsonLineBuffer = lines.pop() ?? '';
|
||||
await throwIfCancelled();
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!response.body) {
|
||||
reject(new Error('command-runner Codex 스트림이 비어 있습니다.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
continue;
|
||||
chatRuntimeService.appendLog(requestId, 'Codex 실행을 command-runner API로 요청했습니다.');
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let remoteErrorMessage = '';
|
||||
|
||||
const handleRunnerLine = (line: string) => {
|
||||
let parsed: Record<string, unknown>;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(line) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRunnerLine(line);
|
||||
const eventType = typeof parsed.type === 'string' ? parsed.type : '';
|
||||
|
||||
if (eventType === 'started') {
|
||||
const pid = typeof parsed.pid === 'number' && Number.isFinite(parsed.pid) ? parsed.pid : null;
|
||||
const appliedIdleTimeoutSeconds =
|
||||
typeof parsed.configuredIdleTimeoutSeconds === 'number' &&
|
||||
Number.isFinite(parsed.configuredIdleTimeoutSeconds)
|
||||
? Math.round(parsed.configuredIdleTimeoutSeconds)
|
||||
: null;
|
||||
const appliedMaxExecutionSeconds =
|
||||
typeof parsed.configuredMaxExecutionSeconds === 'number' &&
|
||||
Number.isFinite(parsed.configuredMaxExecutionSeconds)
|
||||
? Math.round(parsed.configuredMaxExecutionSeconds)
|
||||
: null;
|
||||
chatRuntimeService.attachProcess(requestId, pid);
|
||||
chatRuntimeService.appendLog(
|
||||
requestId,
|
||||
pid
|
||||
? `호스트 command-runner에서 Codex 프로세스를 시작했습니다. pid=${pid}`
|
||||
: '호스트 command-runner에서 Codex 프로세스를 시작했습니다.',
|
||||
);
|
||||
if (appliedMaxExecutionSeconds != null || appliedIdleTimeoutSeconds != null) {
|
||||
const appliedSummary =
|
||||
`command-runner 적용값: 최대 ${appliedMaxExecutionSeconds ?? codexLiveMaxExecutionSeconds ?? 600}초 / ` +
|
||||
`무출력 실패 ${appliedIdleTimeoutSeconds ?? codexLiveIdleTimeoutSeconds ?? 180}초`;
|
||||
chatRuntimeService.appendLog(requestId, appliedSummary);
|
||||
onActivity?.(`# ${appliedSummary}`);
|
||||
|
||||
if (
|
||||
(appliedMaxExecutionSeconds != null &&
|
||||
codexLiveMaxExecutionSeconds != null &&
|
||||
appliedMaxExecutionSeconds !== codexLiveMaxExecutionSeconds) ||
|
||||
(appliedIdleTimeoutSeconds != null &&
|
||||
codexLiveIdleTimeoutSeconds != null &&
|
||||
appliedIdleTimeoutSeconds !== codexLiveIdleTimeoutSeconds)
|
||||
) {
|
||||
const mismatchSummary =
|
||||
`설정 불일치 감지: 요청값 최대 ${codexLiveMaxExecutionSeconds ?? 600}초 / ` +
|
||||
`무출력 실패 ${codexLiveIdleTimeoutSeconds ?? 180}초, ` +
|
||||
`실제 적용값 최대 ${appliedMaxExecutionSeconds ?? codexLiveMaxExecutionSeconds ?? 600}초 / ` +
|
||||
`무출력 실패 ${appliedIdleTimeoutSeconds ?? codexLiveIdleTimeoutSeconds ?? 180}초`;
|
||||
chatRuntimeService.appendLog(requestId, mismatchSummary);
|
||||
onActivity?.(`# 경고: ${mismatchSummary}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'activity') {
|
||||
const activityLog = String(parsed.line ?? '').trim();
|
||||
|
||||
if (activityLog) {
|
||||
chatRuntimeService.appendLog(requestId, activityLog);
|
||||
onActivity?.(activityLog);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'delta') {
|
||||
const deltaText = String(parsed.text ?? '');
|
||||
|
||||
if (deltaText) {
|
||||
hasIncrementalDelta = true;
|
||||
emitProgress(`${streamedOutput}${deltaText}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'completed') {
|
||||
completedAgentMessage = String(parsed.text ?? '').trim();
|
||||
if (completedAgentMessage && hasIncrementalDelta) {
|
||||
emitProgress(completedAgentMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'stdout') {
|
||||
const stdoutLine = String(parsed.line ?? '').trim();
|
||||
|
||||
if (stdoutLine) {
|
||||
stdoutTail = `${stdoutTail}\n${stdoutLine}`.slice(-STREAM_CAPTURE_LIMIT);
|
||||
chatRuntimeService.appendLog(requestId, `[stdout] ${stdoutLine}`);
|
||||
onActivity?.(`[stdout] ${stdoutLine}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'stderr') {
|
||||
const stderrLine = String(parsed.line ?? '').trim();
|
||||
|
||||
if (stderrLine) {
|
||||
stderr = `${stderr}\n${stderrLine}`.slice(-STREAM_CAPTURE_LIMIT);
|
||||
chatRuntimeService.appendLog(requestId, `[stderr] ${stderrLine}`);
|
||||
onActivity?.(`[stderr] ${stderrLine}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType === 'error') {
|
||||
remoteErrorMessage = String(parsed.message ?? '').trim();
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
await throwIfCancelled();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
jsonLineBuffer += decoder.decode(value, { stream: true });
|
||||
const lines = jsonLineBuffer.split('\n');
|
||||
jsonLineBuffer = lines.pop() ?? '';
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
handleRunnerLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const trailingLine = jsonLineBuffer.trim();
|
||||
if (trailingLine) {
|
||||
handleRunnerLine(trailingLine);
|
||||
}
|
||||
const trailingLine = jsonLineBuffer.trim();
|
||||
if (trailingLine) {
|
||||
handleRunnerLine(trailingLine);
|
||||
}
|
||||
|
||||
if (remoteErrorMessage) {
|
||||
reject(new Error(remoteErrorMessage));
|
||||
return;
|
||||
}
|
||||
if (remoteErrorMessage) {
|
||||
reject(new Error(remoteErrorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const failureResponseText = await finalizeReplyOutput();
|
||||
|
||||
if (failureResponseText) {
|
||||
throw new ChatRuntimeExecutionError(error instanceof Error ? error.message : 'Codex 실행에 실패했습니다.', failureResponseText);
|
||||
}
|
||||
});
|
||||
|
||||
const normalizedOutput = normalizeCodexReplyOutput(completedAgentMessage || streamedOutput || stdoutTail);
|
||||
const rewrittenOutput = await rewriteCodexOutputWithChatResources(normalizedOutput, repoPath, sessionId);
|
||||
|
||||
// If the CLI only produced a final completed event, avoid sending it as one big batch.
|
||||
if (!hasIncrementalDelta && rewrittenOutput) {
|
||||
await streamReplyChunks(rewrittenOutput, onProgress);
|
||||
} else if (rewrittenOutput !== lastProgressText) {
|
||||
onProgress?.(rewrittenOutput);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return rewrittenOutput;
|
||||
return await finalizeReplyOutput();
|
||||
}
|
||||
|
||||
async function getTodayAutomationRegistrationCounts() {
|
||||
@@ -3066,6 +3172,15 @@ export class ChatService {
|
||||
}),
|
||||
...contextOverride,
|
||||
};
|
||||
void updateChatConversationContext(state.sessionId, {
|
||||
clientId: state.clientId,
|
||||
chatTypeId: state.context.chatTypeId ?? null,
|
||||
lastChatTypeId: state.context.chatTypeId ?? null,
|
||||
contextLabel: state.context.chatTypeLabel ?? null,
|
||||
contextDescription: state.context.chatTypeDescription ?? null,
|
||||
}).catch((error: unknown) => {
|
||||
this.logger.error(error, 'failed to persist chat context from message send');
|
||||
});
|
||||
}
|
||||
|
||||
this.normalizeSessionExecutionState(state);
|
||||
@@ -3078,6 +3193,7 @@ export class ChatService {
|
||||
text: trimmed,
|
||||
mode,
|
||||
requestedAtMs,
|
||||
context: cloneChatContext(state.context),
|
||||
};
|
||||
|
||||
if (mode === 'queue' && (state.activeRequestCount > 0 || state.queue.length > 0)) {
|
||||
@@ -3129,6 +3245,7 @@ export class ChatService {
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
requestedAtMs: number;
|
||||
context: ChatContext | null;
|
||||
},
|
||||
) {
|
||||
let terminalStatus: 'completed' | 'failed' | 'cancelled' = 'completed';
|
||||
@@ -3258,7 +3375,7 @@ export class ChatService {
|
||||
});
|
||||
|
||||
const reply = await buildCodexReply(
|
||||
session.context ?? null,
|
||||
request.context ?? session.context ?? null,
|
||||
request.text,
|
||||
session.sessionId,
|
||||
request.requestId,
|
||||
@@ -3334,6 +3451,37 @@ export class ChatService {
|
||||
} catch (error) {
|
||||
const wasCancelled = this.cancelledRequestIds.has(request.requestId);
|
||||
terminalStatus = wasCancelled ? 'cancelled' : 'failed';
|
||||
const failureResponseText =
|
||||
error instanceof ChatRuntimeExecutionError ? error.responseText : '';
|
||||
|
||||
if (failureResponseText) {
|
||||
const failedCodexReplyMessage = {
|
||||
...codexReplyMessage,
|
||||
text: failureResponseText,
|
||||
timestamp: resolveResponseTimestamp(request.requestedAtMs),
|
||||
};
|
||||
|
||||
this.sendToSession(
|
||||
session,
|
||||
{
|
||||
type: 'chat:message',
|
||||
payload: failedCodexReplyMessage,
|
||||
},
|
||||
{
|
||||
skipOfflineNotification: true,
|
||||
},
|
||||
);
|
||||
|
||||
await this.persistConversationMessage(session, failedCodexReplyMessage);
|
||||
await upsertChatConversationRequest(session.sessionId, {
|
||||
requestId: request.requestId,
|
||||
status: wasCancelled ? 'cancelled' : 'failed',
|
||||
statusMessage: wasCancelled ? '요청 실행 중단' : '요청 처리 실패',
|
||||
responseMessageId: failedCodexReplyMessage.id,
|
||||
responseText: failedCodexReplyMessage.text,
|
||||
});
|
||||
}
|
||||
|
||||
chatRuntimeService.appendLog(
|
||||
request.requestId,
|
||||
wasCancelled
|
||||
|
||||
Reference in New Issue
Block a user