feat: update codex live runtime and restart flow
This commit is contained in:
@@ -1205,6 +1205,24 @@ export function extractDiffCodeBlocks(output: string) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function protectDiffCodeBlocks(output: string) {
|
||||
const blocks: string[] = [];
|
||||
const text = String(output ?? '').replace(/```diff[^\n]*\n[\s\S]*?\n```/g, (match) => {
|
||||
const token = `__CODEX_DIFF_BLOCK_${blocks.length}__`;
|
||||
blocks.push(match);
|
||||
return token;
|
||||
});
|
||||
|
||||
return { text, blocks };
|
||||
}
|
||||
|
||||
function restoreDiffCodeBlocks(output: string, blocks: string[]) {
|
||||
return blocks.reduce(
|
||||
(current, block, index) => current.replace(`__CODEX_DIFF_BLOCK_${index}__`, block),
|
||||
String(output ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveChatResourceSourcePath(repoPath: string, candidate: string) {
|
||||
const cleaned = stripTrailingLineInfo(trimPathCandidate(candidate.trim()));
|
||||
|
||||
@@ -1323,6 +1341,7 @@ function appendDiffResourceLinks(output: string, diffUrls: string[]) {
|
||||
}
|
||||
|
||||
export async function rewriteCodexOutputWithChatResources(output: string, repoPath: string, sessionId: string) {
|
||||
const { text: outputWithoutDiffBlocks, blocks: diffBlocks } = protectDiffCodeBlocks(output);
|
||||
const escapedRepoPath = escapeRegExp(path.resolve(repoPath));
|
||||
const filePathPattern = "[^\\n\\s)\\]\"'`,]+";
|
||||
const rootFilePattern = String.raw`\/?(?:docker-compose\.(?:yml|yaml)|[A-Za-z0-9._-]*Dockerfile(?:\.[A-Za-z0-9._-]+)?|(?:AGENTS|README)(?:\.[A-Za-z0-9._-]+)?|package(?:-lock)?\.json|tsconfig(?:\.[A-Za-z0-9._-]+)?\.json|vite\.config\.[A-Za-z0-9._-]+|eslint\.config\.[A-Za-z0-9._-]+|prettier\.config\.[A-Za-z0-9._-]+|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|npm-shrinkwrap\.json|\.env(?:\.[A-Za-z0-9._-]+)?|\.gitignore|\.dockerignore)`;
|
||||
@@ -1330,39 +1349,36 @@ export async function rewriteCodexOutputWithChatResources(output: string, repoPa
|
||||
`${escapedRepoPath}\\/${filePathPattern}|(?:\\/?(?:public\\/)?\\.codex_chat|src|public|docs|etc|scripts)\\/${filePathPattern}|${rootFilePattern}`,
|
||||
'g',
|
||||
);
|
||||
const matches = [...output.matchAll(candidatePattern)];
|
||||
const matches = [...outputWithoutDiffBlocks.matchAll(candidatePattern)];
|
||||
let rewrittenOutput = outputWithoutDiffBlocks;
|
||||
if (matches.length > 0) {
|
||||
const replacementMap = new Map<string, string>();
|
||||
|
||||
if (matches.length === 0) {
|
||||
return output;
|
||||
}
|
||||
for (const match of matches) {
|
||||
const rawCandidate = match[0]?.trim();
|
||||
|
||||
const replacementMap = new Map<string, string>();
|
||||
if (!rawCandidate || replacementMap.has(rawCandidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
const rawCandidate = match[0]?.trim();
|
||||
const stagedUrl = await stageChatResourceFile(repoPath, sessionId, rawCandidate);
|
||||
|
||||
if (!rawCandidate || replacementMap.has(rawCandidate)) {
|
||||
continue;
|
||||
if (stagedUrl) {
|
||||
replacementMap.set(rawCandidate, stagedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const stagedUrl = await stageChatResourceFile(repoPath, sessionId, rawCandidate);
|
||||
const replacements = Array.from(replacementMap.entries()).sort((left, right) => right[0].length - left[0].length);
|
||||
|
||||
if (stagedUrl) {
|
||||
replacementMap.set(rawCandidate, stagedUrl);
|
||||
for (const [sourcePath, publicUrl] of replacements) {
|
||||
rewrittenOutput = rewrittenOutput.replaceAll(sourcePath, publicUrl);
|
||||
}
|
||||
}
|
||||
|
||||
let rewrittenOutput = output;
|
||||
|
||||
const replacements = Array.from(replacementMap.entries()).sort((left, right) => right[0].length - left[0].length);
|
||||
|
||||
for (const [sourcePath, publicUrl] of replacements) {
|
||||
rewrittenOutput = rewrittenOutput.replaceAll(sourcePath, publicUrl);
|
||||
}
|
||||
|
||||
rewrittenOutput = normalizeEmbeddedChatResourceUrls(rewrittenOutput);
|
||||
rewrittenOutput = restoreDiffCodeBlocks(rewrittenOutput, diffBlocks);
|
||||
|
||||
const diffUrls = await stageChatDiffResourceFiles(repoPath, sessionId, rewrittenOutput);
|
||||
const diffUrls = await stageChatDiffResourceFiles(repoPath, sessionId, output);
|
||||
return appendDiffResourceLinks(rewrittenOutput, diffUrls);
|
||||
}
|
||||
|
||||
@@ -1648,6 +1664,7 @@ async function runAgenticCodexReply(
|
||||
requestId: string,
|
||||
onProgress?: (text: string) => void,
|
||||
onActivity?: (line: string) => void,
|
||||
isCancellationRequested?: () => boolean,
|
||||
) {
|
||||
const repoPath = env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || env.PLAN_GIT_REPO_PATH;
|
||||
await validateAgenticCodexRuntime(repoPath, env.PLAN_CODEX_BIN);
|
||||
@@ -1670,8 +1687,21 @@ async function runAgenticCodexReply(
|
||||
let lastProgressText = '';
|
||||
let completedAgentMessage = '';
|
||||
let hasIncrementalDelta = false;
|
||||
const throwIfCancelled = async () => {
|
||||
if (!isCancellationRequested?.()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await cancelRunnerCodexExecution(requestId).catch(() => false);
|
||||
throw new Error('CHAT_RUNTIME_CANCELLED');
|
||||
};
|
||||
|
||||
await throwIfCancelled();
|
||||
activeChatProcessRegistry.set(requestId, {
|
||||
cancel: () => cancelRunnerCodexExecution(requestId),
|
||||
cancel: async () => {
|
||||
const cancelled = await cancelRunnerCodexExecution(requestId);
|
||||
return cancelled || isCancellationRequested?.() === true;
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
@@ -1688,6 +1718,7 @@ async function runAgenticCodexReply(
|
||||
};
|
||||
|
||||
try {
|
||||
await throwIfCancelled();
|
||||
const response = await requestCommandRunner('/api/codex-live/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -1705,6 +1736,8 @@ async function runAgenticCodexReply(
|
||||
return;
|
||||
}
|
||||
|
||||
await throwIfCancelled();
|
||||
|
||||
if (!response.body) {
|
||||
reject(new Error('command-runner Codex 스트림이 비어 있습니다.'));
|
||||
return;
|
||||
@@ -1792,6 +1825,7 @@ async function runAgenticCodexReply(
|
||||
};
|
||||
|
||||
while (true) {
|
||||
await throwIfCancelled();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
@@ -2130,8 +2164,17 @@ async function buildCodexReply(
|
||||
requestId: string,
|
||||
onProgress?: (text: string) => void,
|
||||
onActivity?: (line: string) => void,
|
||||
isCancellationRequested?: () => boolean,
|
||||
) {
|
||||
return runAgenticCodexReply(context, input, sessionId, requestId, onProgress, onActivity);
|
||||
return runAgenticCodexReply(
|
||||
context,
|
||||
input,
|
||||
sessionId,
|
||||
requestId,
|
||||
onProgress,
|
||||
onActivity,
|
||||
isCancellationRequested,
|
||||
);
|
||||
}
|
||||
|
||||
export class ChatService {
|
||||
@@ -2364,6 +2407,25 @@ export class ChatService {
|
||||
|
||||
private async cancelRuntimeJob(requestId: string) {
|
||||
const execution = activeChatProcessRegistry.get(requestId);
|
||||
const detail = chatRuntimeService.getJobDetail(requestId);
|
||||
|
||||
if (!execution && detail.item && detail.terminalStatus == null) {
|
||||
chatRuntimeService.appendLog(requestId, '실행 준비 단계에서 취소 요청을 접수했습니다.');
|
||||
this.cancelledRequestIds.add(requestId);
|
||||
const session = this.findSessionByRequestId(requestId);
|
||||
|
||||
if (session) {
|
||||
void upsertChatConversationRequest(session.sessionId, {
|
||||
requestId,
|
||||
status: 'cancelled',
|
||||
statusMessage: '사용자 요청으로 실행 취소를 대기합니다.',
|
||||
}).catch((error: unknown) => {
|
||||
this.logger.warn(error, 'failed to persist pending chat request cancellation state');
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!execution) {
|
||||
return false;
|
||||
@@ -2384,7 +2446,14 @@ export class ChatService {
|
||||
}
|
||||
|
||||
try {
|
||||
return await execution.cancel();
|
||||
const cancelled = await execution.cancel();
|
||||
|
||||
if (!cancelled && this.cancelledRequestIds.has(requestId)) {
|
||||
chatRuntimeService.appendLog(requestId, '취소 신호를 재시도 대기 중입니다.');
|
||||
return true;
|
||||
}
|
||||
|
||||
return cancelled;
|
||||
} catch (error) {
|
||||
this.logger.warn(error, 'failed to cancel chat runtime job');
|
||||
chatRuntimeService.appendLog(requestId, '실행 취소 요청에 실패했습니다.');
|
||||
@@ -3221,6 +3290,7 @@ export class ChatService {
|
||||
(activityLine) => {
|
||||
appendActivityLine(activityLine);
|
||||
},
|
||||
() => this.cancelledRequestIds.has(request.requestId),
|
||||
);
|
||||
chatRuntimeService.appendLog(request.requestId, '응답 생성이 완료되었습니다.');
|
||||
appendActivityLine('# 상태: 응답 생성이 완료되었습니다.');
|
||||
|
||||
Reference in New Issue
Block a user