feat: refine codex live chat context flows

This commit is contained in:
2026-05-08 21:15:51 +09:00
parent 82c0d8a197
commit 442879313f
92 changed files with 14815 additions and 7314 deletions

View File

@@ -50,6 +50,8 @@ export type ChatConversationItem = {
currentJobMessage: string | null;
currentQueueSize: number;
currentStatusUpdatedAt: string | null;
isPendingWork: boolean;
pendingWorkReason: 'prompt' | 'analysis' | 'design' | null;
lastRequestPreview: string;
lastMessagePreview: string;
lastResponsePreview: string;
@@ -173,6 +175,160 @@ function createPreview(text: string) {
return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
}
const PENDING_WORK_ANALYSIS_PATTERNS = [
//u,
//u,
//u,
//u,
//u,
/\banalysis\b/i,
/\binvestigat(?:e|ion)\b/i,
] as const;
const PENDING_WORK_DESIGN_PATTERNS = [
//u,
//u,
//u,
//u,
//u,
//u,
//u,
//u,
/\bdesign\b/i,
/\barchitecture\b/i,
] as const;
const PENDING_WORK_IMPLEMENTATION_PATTERNS = [
//u,
//u,
//u,
//u,
//u,
//u,
//u,
//u,
/.*/u,
/.*/u,
//u,
/preview/iu,
/ /u,
/diff/u,
/\bimplement(?:ed|ation)?\b/i,
/\bfix(?:ed)?\b/i,
/\bverified?\b/i,
/\btested?\b/i,
] as const;
const PENDING_WORK_RESPONSE_HOLD_PATTERNS = [
//u,
//u,
/(?:|)/u,
/ /u,
//u,
//u,
//u,
/\bif you want\b/i,
/\bnext step\b/i,
] as const;
function normalizePendingWorkText(text: string | null | undefined) {
return String(text ?? '').replace(/\s+/g, ' ').trim();
}
function hasPendingWorkPattern(text: string, patterns: readonly RegExp[]) {
return patterns.some((pattern) => pattern.test(text));
}
function resolvePendingWorkReasonFromText(text: string) {
if (!text) {
return null;
}
if (hasPendingWorkPattern(text, PENDING_WORK_DESIGN_PATTERNS)) {
return 'design' as const;
}
if (hasPendingWorkPattern(text, PENDING_WORK_ANALYSIS_PATTERNS)) {
return 'analysis' as const;
}
return null;
}
function hasOpenPromptParts(parts: ChatMessagePart[] | undefined) {
return (parts ?? []).some((part) => {
if (part.type !== 'prompt' || part.readOnly === true) {
return false;
}
if ((part.selectedValues?.length ?? 0) > 0) {
return false;
}
if ((part.resultText?.trim() ?? '').length > 0) {
return false;
}
if ((part.resolvedAt?.trim() ?? '').length > 0 || part.resolvedBy != null) {
return false;
}
return true;
});
}
function resolvePendingWorkState(args: {
requestText?: string | null;
responseText?: string | null;
latestCodexParts?: ChatMessagePart[] | undefined;
}) {
if (hasOpenPromptParts(args.latestCodexParts)) {
return {
isPendingWork: true,
pendingWorkReason: 'prompt' as const,
};
}
const requestText = normalizePendingWorkText(args.requestText);
const responseText = normalizePendingWorkText(args.responseText);
const requestReason = resolvePendingWorkReasonFromText(requestText);
if (!requestReason) {
return {
isPendingWork: false,
pendingWorkReason: null,
};
}
if (hasPendingWorkPattern(responseText, PENDING_WORK_IMPLEMENTATION_PATTERNS)) {
return {
isPendingWork: false,
pendingWorkReason: null,
};
}
if (!responseText) {
return {
isPendingWork: true,
pendingWorkReason: requestReason,
};
}
const responseReason = resolvePendingWorkReasonFromText(responseText);
if (responseReason || hasPendingWorkPattern(responseText, PENDING_WORK_RESPONSE_HOLD_PATTERNS)) {
return {
isPendingWork: true,
pendingWorkReason: responseReason ?? requestReason,
};
}
return {
isPendingWork: false,
pendingWorkReason: null,
};
}
const CONTEXT_DEPENDENT_REQUEST_PATTERNS = [
/\s*(||)/u,
/\s*/u,
@@ -279,6 +435,8 @@ function mapConversationRow(row: Record<string, unknown>): ChatConversationItem
currentJobMessage: row.current_job_message == null ? null : String(row.current_job_message),
currentQueueSize: Number(row.current_queue_size ?? 0),
currentStatusUpdatedAt: normalizeDateTimeValue(row.current_status_updated_at),
isPendingWork: false,
pendingWorkReason: null,
lastRequestPreview: '',
lastMessagePreview: String(row.last_message_preview ?? ''),
lastResponsePreview: '',
@@ -876,6 +1034,40 @@ async function getLatestResponseMessageIdMap(sessionIds: string[]) {
return responseMap;
}
async function getLatestCodexPromptPartsMap(sessionIds: string[]) {
const normalizedSessionIds = Array.from(new Set(sessionIds.map((sessionId) => sessionId.trim()).filter(Boolean)));
if (normalizedSessionIds.length === 0) {
return new Map<string, ChatMessagePart[]>();
}
const rows = await db(CHAT_CONVERSATION_MESSAGE_TABLE)
.select('session_id', 'parts_json', 'created_at', 'message_id')
.whereIn('session_id', normalizedSessionIds)
.andWhere('author', 'codex')
.orderBy('session_id', 'asc')
.orderBy('created_at', 'desc')
.orderBy('message_id', 'desc');
const promptPartMap = new Map<string, ChatMessagePart[]>();
for (const row of rows) {
const sessionId = String(row.session_id ?? '').trim();
if (!sessionId || promptPartMap.has(sessionId)) {
continue;
}
const parts = parseChatMessageParts(row.parts_json);
if ((parts ?? []).some((part) => part.type === 'prompt')) {
promptPartMap.set(sessionId, parts ?? []);
}
}
return promptPartMap;
}
async function getLatestResponseMessageId(sessionId: string) {
const responseMap = await getLatestResponseMessageIdMap([sessionId]);
return responseMap.get(sessionId.trim()) ?? null;
@@ -1444,17 +1636,26 @@ export async function listChatConversations(
const latestResponseMessageIdMap = await getLatestResponseMessageIdMap(
rows.map((row) => String(row.session_id ?? '')),
);
const latestCodexPromptPartsMap = await getLatestCodexPromptPartsMap(
rows.map((row) => String(row.session_id ?? '')),
);
if (!normalizedUnreadStateClientId) {
return rows
.map((row) => {
const mapped = mapConversationRow(row);
const pendingWorkState = resolvePendingWorkState({
requestText: latestRequestPreviewMap.get(mapped.sessionId)?.text ?? '',
responseText: latestResponsePreviewMap.get(mapped.sessionId)?.text ?? '',
latestCodexParts: latestCodexPromptPartsMap.get(mapped.sessionId),
});
return {
...resolveConversationPreviewOverride(
mapped,
latestPreviewMessageMap.get(mapped.sessionId),
latestRequestPreviewMap.get(mapped.sessionId),
),
...pendingWorkState,
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
hasUnreadResponse: false,
@@ -1489,6 +1690,11 @@ export async function listChatConversations(
const mapped = mapConversationRow(row);
const preference = preferenceMap.get(mapped.sessionId);
const latestPreviewMessage = latestPreviewMessageMap.get(mapped.sessionId);
const pendingWorkState = resolvePendingWorkState({
requestText: latestRequestPreviewMap.get(mapped.sessionId)?.text ?? '',
responseText: latestResponsePreviewMap.get(mapped.sessionId)?.text ?? '',
latestCodexParts: latestCodexPromptPartsMap.get(mapped.sessionId),
});
return {
...resolveConversationPreviewOverride(
@@ -1496,6 +1702,7 @@ export async function listChatConversations(
latestPreviewMessage,
latestRequestPreviewMap.get(mapped.sessionId),
),
...pendingWorkState,
lastRequestPreview: createPreview(latestRequestPreviewMap.get(mapped.sessionId)?.text ?? ''),
lastResponsePreview: createPreview(latestResponsePreviewMap.get(mapped.sessionId)?.text ?? ''),
clientId: normalizedUnreadStateClientId,
@@ -1654,7 +1861,7 @@ export async function listChatConversationDetailPage(
): Promise<ChatConversationDetailPage> {
const normalizedSessionId = sessionId.trim();
const conversation = await db(CHAT_CONVERSATION_TABLE).where({ session_id: normalizedSessionId }).first();
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 6)));
const normalizedLimit = Math.max(1, Math.min(100, Math.round(options.limit ?? 8)));
const normalizedBeforeMessageId =
Number.isFinite(options.beforeMessageId) && (options.beforeMessageId ?? 0) > 0
? Math.trunc(options.beforeMessageId as number)
@@ -2435,6 +2642,36 @@ export async function deleteChatConversation(sessionId: string) {
});
}
export async function clearChatConversationData(sessionId: string, clientId?: string | null) {
const normalizedSessionId = sessionId.trim();
await db.transaction(async (trx) => {
await trx(CHAT_CONVERSATION_ACTIVITY_TABLE).where({ session_id: normalizedSessionId }).del();
await trx(CHAT_CONVERSATION_REQUEST_TABLE).where({ session_id: normalizedSessionId }).del();
await trx(CHAT_CONVERSATION_MESSAGE_TABLE).where({ session_id: normalizedSessionId }).del();
await trx(CHAT_CONVERSATION_CLIENT_TABLE)
.where({ session_id: normalizedSessionId })
.update({
last_read_response_message_id: null,
updated_at: db.fn.now(),
});
await trx(CHAT_CONVERSATION_TABLE)
.where({ session_id: normalizedSessionId })
.update({
current_request_id: null,
current_job_status: null,
current_job_message: null,
current_queue_size: 0,
current_status_updated_at: null,
last_message_preview: '',
last_message_at: null,
updated_at: db.fn.now(),
});
});
return getChatConversation(normalizedSessionId, clientId);
}
export async function getChatConversationClientPreference(sessionId: string, clientId: string) {
const row = await db(CHAT_CONVERSATION_CLIENT_TABLE)
.where({