dr-claw 1.1.2
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/LICENSE +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- package/shared/modelConstants.js +139 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex SDK Integration
|
|
3
|
+
* =============================
|
|
4
|
+
*
|
|
5
|
+
* This module provides integration with the OpenAI Codex SDK for non-interactive
|
|
6
|
+
* chat sessions. It mirrors the pattern used in claude-sdk.js for consistency.
|
|
7
|
+
*
|
|
8
|
+
* ## Usage
|
|
9
|
+
*
|
|
10
|
+
* - queryCodex(command, options, ws) - Execute a prompt with streaming via WebSocket
|
|
11
|
+
* - abortCodexSession(sessionId) - Cancel an active session
|
|
12
|
+
* - isCodexSessionActive(sessionId) - Check if a session is running
|
|
13
|
+
* - getActiveCodexSessions() - List all active sessions
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Codex } from '@openai/codex-sdk';
|
|
17
|
+
import { promises as fs } from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { encodeProjectPath, reconcileCodexSessionIndex } from './projects.js';
|
|
20
|
+
import { sessionDb } from './database/db.js';
|
|
21
|
+
import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIndex.js';
|
|
22
|
+
import { classifyError, classifySDKError } from '../shared/errorClassifier.js';
|
|
23
|
+
import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
|
|
24
|
+
import { buildCodexRealtimeTokenBudget } from './utils/sessionTokenUsage.js';
|
|
25
|
+
|
|
26
|
+
// Track active sessions
|
|
27
|
+
const activeCodexSessions = new Map();
|
|
28
|
+
|
|
29
|
+
function moveActiveCodexSession(oldSessionId, newSessionId) {
|
|
30
|
+
if (!oldSessionId || !newSessionId || oldSessionId === newSessionId) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const session = activeCodexSessions.get(oldSessionId);
|
|
35
|
+
if (!session) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
activeCodexSessions.set(newSessionId, session);
|
|
40
|
+
activeCodexSessions.delete(oldSessionId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if an agent_message item contains system prompt / instruction content
|
|
45
|
+
* that should be collapsed rather than displayed as a normal message.
|
|
46
|
+
* @param {string} text - The message text
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function isSystemPromptContent(text) {
|
|
50
|
+
if (!text || text.length < 200) return false;
|
|
51
|
+
// AGENTS.md / SKILL.md / INSTRUCTIONS headers
|
|
52
|
+
if (/^#\s+(AGENTS|SKILL|INSTRUCTIONS)/m.test(text)) return true;
|
|
53
|
+
// XML instruction tags
|
|
54
|
+
if (text.includes('<INSTRUCTIONS>') || text.includes('</INSTRUCTIONS>')) return true;
|
|
55
|
+
// "instructions for /path" pattern in a heading
|
|
56
|
+
if (/^#+\s+.*instructions\s+for\s+\//im.test(text)) return true;
|
|
57
|
+
// Skill content markers
|
|
58
|
+
if (text.includes('Base directory for this skill:') && text.length > 500) return true;
|
|
59
|
+
// Long text with numbered-list instruction patterns
|
|
60
|
+
if (text.length > 2000 && /^\d+\)\s/m.test(text) && /\bskill\b/i.test(text)) return true;
|
|
61
|
+
// Repeated SKILL.md file paths (skill listing content)
|
|
62
|
+
const skillPathCount = (text.match(/SKILL\.md\)/g) || []).length;
|
|
63
|
+
if (skillPathCount >= 3) return true;
|
|
64
|
+
// "How to use skills" section
|
|
65
|
+
if (text.includes('### How to use skills') || text.includes('## How to use skills')) return true;
|
|
66
|
+
// Skill discovery/trigger rules pattern
|
|
67
|
+
if (text.includes('Trigger rules:') && text.includes('skill') && text.length > 500) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transform Codex SDK event to WebSocket message format
|
|
73
|
+
* @param {object} event - SDK event
|
|
74
|
+
* @returns {object|null} - Transformed event for WebSocket, or null to skip
|
|
75
|
+
*/
|
|
76
|
+
function transformCodexEvent(event) {
|
|
77
|
+
// Map SDK event types to a consistent format
|
|
78
|
+
switch (event.type) {
|
|
79
|
+
case 'item.started':
|
|
80
|
+
case 'item.updated':
|
|
81
|
+
case 'item.completed':
|
|
82
|
+
const item = event.item;
|
|
83
|
+
if (!item) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Transform based on item type
|
|
88
|
+
switch (item.type) {
|
|
89
|
+
case 'agent_message': {
|
|
90
|
+
const text = item.text || '';
|
|
91
|
+
if (!text.trim()) return null;
|
|
92
|
+
|
|
93
|
+
// Detect and mark system prompt content
|
|
94
|
+
const isSysPrompt = isSystemPromptContent(text);
|
|
95
|
+
return {
|
|
96
|
+
type: 'item',
|
|
97
|
+
itemType: 'agent_message',
|
|
98
|
+
message: {
|
|
99
|
+
role: 'assistant',
|
|
100
|
+
content: text
|
|
101
|
+
},
|
|
102
|
+
isSystemPrompt: isSysPrompt
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'reasoning': {
|
|
107
|
+
// Codex reasoning items are brief status notes with no real value to display
|
|
108
|
+
// Skip them entirely to avoid "💭 Thinking..." spam in the UI
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'command_execution': {
|
|
113
|
+
// Codex may wrap commands in JSON: {"cmd":"...", "workdir":"...", "max_output_tokens":...}
|
|
114
|
+
// Extract just the command string for display
|
|
115
|
+
let command = item.command || '';
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(command);
|
|
118
|
+
if (parsed.cmd) command = parsed.cmd;
|
|
119
|
+
} catch {
|
|
120
|
+
// Not JSON, use as-is
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
type: 'item',
|
|
124
|
+
itemType: 'command_execution',
|
|
125
|
+
command,
|
|
126
|
+
output: item.aggregated_output || '',
|
|
127
|
+
exitCode: item.exit_code,
|
|
128
|
+
status: item.status
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'file_change':
|
|
133
|
+
return {
|
|
134
|
+
type: 'item',
|
|
135
|
+
itemType: 'file_change',
|
|
136
|
+
changes: item.changes,
|
|
137
|
+
status: item.status
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
case 'mcp_tool_call':
|
|
141
|
+
return {
|
|
142
|
+
type: 'item',
|
|
143
|
+
itemType: 'mcp_tool_call',
|
|
144
|
+
server: item.server,
|
|
145
|
+
tool: item.tool,
|
|
146
|
+
arguments: item.arguments,
|
|
147
|
+
result: item.result,
|
|
148
|
+
error: item.error,
|
|
149
|
+
status: item.status
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
case 'web_search':
|
|
153
|
+
return {
|
|
154
|
+
type: 'item',
|
|
155
|
+
itemType: 'web_search',
|
|
156
|
+
query: item.query || ''
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
case 'todo_list':
|
|
160
|
+
return {
|
|
161
|
+
type: 'item',
|
|
162
|
+
itemType: 'todo_list',
|
|
163
|
+
items: item.items
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
case 'error':
|
|
167
|
+
return {
|
|
168
|
+
type: 'item',
|
|
169
|
+
itemType: 'error',
|
|
170
|
+
message: {
|
|
171
|
+
role: 'error',
|
|
172
|
+
content: item.message
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
return {
|
|
178
|
+
type: 'item',
|
|
179
|
+
itemType: item.type,
|
|
180
|
+
item: item
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'turn.started':
|
|
185
|
+
return {
|
|
186
|
+
type: 'turn_started'
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
case 'turn.completed':
|
|
190
|
+
return {
|
|
191
|
+
type: 'turn_complete',
|
|
192
|
+
usage: event.usage
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
case 'turn.failed':
|
|
196
|
+
return {
|
|
197
|
+
type: 'turn_failed',
|
|
198
|
+
error: event.error
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
case 'thread.started':
|
|
202
|
+
return {
|
|
203
|
+
type: 'thread_started',
|
|
204
|
+
threadId: event.id
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
case 'error':
|
|
208
|
+
return {
|
|
209
|
+
type: 'error',
|
|
210
|
+
message: event.message
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return {
|
|
215
|
+
type: event.type,
|
|
216
|
+
data: event
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Map permission mode to Codex SDK options
|
|
223
|
+
* @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
|
|
224
|
+
* @returns {object} - { sandboxMode, approvalPolicy }
|
|
225
|
+
*/
|
|
226
|
+
function mapPermissionModeToCodexOptions(permissionMode) {
|
|
227
|
+
switch (permissionMode) {
|
|
228
|
+
case 'acceptEdits':
|
|
229
|
+
return {
|
|
230
|
+
sandboxMode: 'workspace-write',
|
|
231
|
+
approvalPolicy: 'never'
|
|
232
|
+
};
|
|
233
|
+
case 'bypassPermissions':
|
|
234
|
+
return {
|
|
235
|
+
sandboxMode: 'danger-full-access',
|
|
236
|
+
approvalPolicy: 'never'
|
|
237
|
+
};
|
|
238
|
+
case 'default':
|
|
239
|
+
default:
|
|
240
|
+
return {
|
|
241
|
+
sandboxMode: 'workspace-write',
|
|
242
|
+
approvalPolicy: 'untrusted'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildCodexInput(command, attachments) {
|
|
248
|
+
const imagePaths = Array.isArray(attachments?.imagePaths)
|
|
249
|
+
? attachments.imagePaths.filter((value) => typeof value === 'string' && value.trim())
|
|
250
|
+
: [];
|
|
251
|
+
const documentPaths = Array.isArray(attachments?.documentPaths)
|
|
252
|
+
? attachments.documentPaths.filter((value) => typeof value === 'string' && value.trim())
|
|
253
|
+
: [];
|
|
254
|
+
|
|
255
|
+
if (imagePaths.length === 0 && documentPaths.length === 0) {
|
|
256
|
+
return command;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const textSections = [command];
|
|
260
|
+
|
|
261
|
+
if (documentPaths.length > 0) {
|
|
262
|
+
textSections.push(
|
|
263
|
+
`Attached workspace PDF path(s):\n${documentPaths
|
|
264
|
+
.map((filePath) => `- ${filePath}`)
|
|
265
|
+
.join('\n')}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (imagePaths.length > 0) {
|
|
270
|
+
textSections.push(
|
|
271
|
+
imagePaths.length === 1
|
|
272
|
+
? 'An image is attached below.'
|
|
273
|
+
: `There are ${imagePaths.length} attached images below.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [
|
|
278
|
+
{ type: 'text', text: textSections.join('\n\n') },
|
|
279
|
+
...imagePaths.map((filePath) => ({ type: 'local_image', path: filePath })),
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function prepareCodexInput(command, images, cwd) {
|
|
284
|
+
const tempImagePaths = [];
|
|
285
|
+
let tempDir = null;
|
|
286
|
+
|
|
287
|
+
if (!Array.isArray(images) || images.length === 0) {
|
|
288
|
+
return { input: command, tempImagePaths, tempDir };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const workingDir = cwd || process.cwd();
|
|
293
|
+
tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
|
|
294
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
295
|
+
|
|
296
|
+
const input = [{ type: 'text', text: command }];
|
|
297
|
+
|
|
298
|
+
for (const [index, image] of images.entries()) {
|
|
299
|
+
const data = String(image?.data || '');
|
|
300
|
+
const matches = data.match(/^data:([^;]+);base64,(.+)$/);
|
|
301
|
+
if (!matches) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const [, mimeType, base64Data] = matches;
|
|
306
|
+
const filename = buildTempAttachmentFilename(index, image?.name, mimeType);
|
|
307
|
+
const filepath = path.join(tempDir, filename);
|
|
308
|
+
|
|
309
|
+
await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
|
|
310
|
+
tempImagePaths.push(filepath);
|
|
311
|
+
input.push({ type: 'local_image', path: filepath });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
input: input.length > 1 ? input : command,
|
|
316
|
+
tempImagePaths,
|
|
317
|
+
tempDir,
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('[Codex] Failed to prepare image inputs:', error);
|
|
321
|
+
return { input: command, tempImagePaths, tempDir };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function cleanupCodexTempFiles(tempImagePaths, tempDir) {
|
|
326
|
+
if (!Array.isArray(tempImagePaths) || tempImagePaths.length === 0) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const filePath of tempImagePaths) {
|
|
331
|
+
try {
|
|
332
|
+
await fs.unlink(filePath);
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (tempDir) {
|
|
337
|
+
try {
|
|
338
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
339
|
+
} catch {}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Execute a Codex query with streaming
|
|
345
|
+
* @param {string} command - The prompt to send
|
|
346
|
+
* @param {object} options - Options including cwd, sessionId, model, permissionMode, modelReasoningEffort
|
|
347
|
+
* @param {WebSocket|object} ws - WebSocket connection or response writer
|
|
348
|
+
*/
|
|
349
|
+
export async function queryCodex(command, options = {}, ws) {
|
|
350
|
+
const {
|
|
351
|
+
sessionId,
|
|
352
|
+
cwd,
|
|
353
|
+
projectPath,
|
|
354
|
+
model,
|
|
355
|
+
env,
|
|
356
|
+
attachments,
|
|
357
|
+
images,
|
|
358
|
+
permissionMode = 'default',
|
|
359
|
+
modelReasoningEffort,
|
|
360
|
+
sessionMode,
|
|
361
|
+
stageTagKeys,
|
|
362
|
+
stageTagSource = 'task_context',
|
|
363
|
+
} = options;
|
|
364
|
+
|
|
365
|
+
const workingDirectory = cwd || projectPath || process.cwd();
|
|
366
|
+
const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
|
|
367
|
+
|
|
368
|
+
let codex;
|
|
369
|
+
let thread;
|
|
370
|
+
let currentSessionId = sessionId || null;
|
|
371
|
+
let provisionalSessionId = null;
|
|
372
|
+
const abortController = new AbortController();
|
|
373
|
+
let tempImagePaths = [];
|
|
374
|
+
let tempDir = null;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
// Synchronous (better-sqlite3) — no await needed.
|
|
378
|
+
if (sessionId && workingDirectory) {
|
|
379
|
+
applyStageTagsToSession({
|
|
380
|
+
sessionId,
|
|
381
|
+
projectPath: workingDirectory,
|
|
382
|
+
stageTagKeys,
|
|
383
|
+
source: stageTagSource,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Initialize Codex SDK
|
|
388
|
+
codex = new Codex(env ? { env } : undefined);
|
|
389
|
+
|
|
390
|
+
// Thread options with sandbox and approval settings
|
|
391
|
+
const threadOptions = {
|
|
392
|
+
workingDirectory,
|
|
393
|
+
skipGitRepoCheck: true,
|
|
394
|
+
sandboxMode,
|
|
395
|
+
approvalPolicy,
|
|
396
|
+
model,
|
|
397
|
+
modelReasoningEffort,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Start or resume thread
|
|
401
|
+
if (sessionId) {
|
|
402
|
+
thread = codex.resumeThread(sessionId, threadOptions);
|
|
403
|
+
} else {
|
|
404
|
+
thread = codex.startThread(threadOptions);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
provisionalSessionId = currentSessionId || `codex-${Date.now()}`;
|
|
408
|
+
activeCodexSessions.set(provisionalSessionId, {
|
|
409
|
+
thread,
|
|
410
|
+
codex,
|
|
411
|
+
status: 'running',
|
|
412
|
+
abortController,
|
|
413
|
+
startTime: Date.now()
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const publishSessionId = (resolvedSessionId) => {
|
|
417
|
+
if (!resolvedSessionId || resolvedSessionId === currentSessionId) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const previousSessionId = currentSessionId || provisionalSessionId;
|
|
422
|
+
currentSessionId = resolvedSessionId;
|
|
423
|
+
|
|
424
|
+
if (previousSessionId && previousSessionId !== currentSessionId) {
|
|
425
|
+
moveActiveCodexSession(previousSessionId, currentSessionId);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (workingDirectory) {
|
|
429
|
+
recordIndexedSession({
|
|
430
|
+
sessionId: currentSessionId,
|
|
431
|
+
provider: 'codex',
|
|
432
|
+
projectPath: workingDirectory,
|
|
433
|
+
sessionMode: sessionMode || 'research',
|
|
434
|
+
stageTagKeys,
|
|
435
|
+
tagSource: stageTagSource,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
sendMessage(ws, {
|
|
440
|
+
type: 'session-created',
|
|
441
|
+
sessionId: currentSessionId,
|
|
442
|
+
provider: 'codex',
|
|
443
|
+
mode: sessionMode || 'research'
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
publishSessionId(thread.id || sessionId || null);
|
|
448
|
+
|
|
449
|
+
const preparedInput = await prepareCodexInput(command, images, workingDirectory);
|
|
450
|
+
tempImagePaths = preparedInput.tempImagePaths;
|
|
451
|
+
tempDir = preparedInput.tempDir;
|
|
452
|
+
|
|
453
|
+
// Execute with streaming
|
|
454
|
+
// Prefer pre-uploaded attachments (buildCodexInput) over base64 temp images (prepareCodexInput)
|
|
455
|
+
const codexInput = attachments
|
|
456
|
+
? buildCodexInput(command, attachments)
|
|
457
|
+
: preparedInput.input;
|
|
458
|
+
const streamedTurn = await thread.runStreamed(codexInput, {
|
|
459
|
+
signal: abortController.signal
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Track items we've already sent to avoid duplicates
|
|
463
|
+
const sentItems = new Map(); // itemId -> lifecycle stage
|
|
464
|
+
|
|
465
|
+
for await (const event of streamedTurn.events) {
|
|
466
|
+
if (event.type === 'thread.started' && event.id) {
|
|
467
|
+
publishSessionId(event.id);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Check if session was aborted
|
|
471
|
+
const activeLookupId = currentSessionId || provisionalSessionId;
|
|
472
|
+
const session = activeCodexSessions.get(activeLookupId);
|
|
473
|
+
if (!session || session.status === 'aborted') {
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const itemType = event.item?.type || 'unknown';
|
|
478
|
+
const itemId = event.item?.id || null;
|
|
479
|
+
|
|
480
|
+
// Detailed debug logging
|
|
481
|
+
if (event.item) {
|
|
482
|
+
const preview = event.item.text ? event.item.text.substring(0, 80) : (event.item.command || '');
|
|
483
|
+
console.log(`[Codex] ${event.type} | ${itemType} | id=${itemId} | preview="${preview}"`);
|
|
484
|
+
// Extra logging for command_execution output
|
|
485
|
+
if (itemType === 'command_execution' && event.type === 'item.completed') {
|
|
486
|
+
const outLen = event.item.aggregated_output?.length || 0;
|
|
487
|
+
const outPreview = event.item.aggregated_output?.substring(0, 120) || '(empty)';
|
|
488
|
+
console.log(`[Codex] cmd output (${outLen} chars): "${outPreview}"`);
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
console.log(`[Codex] ${event.type}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Event filtering:
|
|
495
|
+
// - item.updated: always skip (streaming noise)
|
|
496
|
+
// - item.started: forward tool-type items immediately so they appear in UI
|
|
497
|
+
// - item.completed: always forward (final state with results)
|
|
498
|
+
if (event.type === 'item.updated') {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (event.type === 'item.started') {
|
|
503
|
+
const toolTypes = new Set(['command_execution', 'file_change', 'mcp_tool_call', 'web_search']);
|
|
504
|
+
if (!toolTypes.has(itemType)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (itemId) sentItems.set(itemId, 'started');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (event.type === 'item.completed' && itemId) {
|
|
511
|
+
sentItems.set(itemId, 'completed');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const transformed = transformCodexEvent(event);
|
|
515
|
+
|
|
516
|
+
// Skip null transforms (empty reasoning, etc.)
|
|
517
|
+
if (!transformed) {
|
|
518
|
+
console.log(`[Codex] Skipped null transform for ${event.type} | ${itemType}`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Add lifecycle info for frontend dedup
|
|
523
|
+
if (itemId) {
|
|
524
|
+
transformed.itemId = itemId;
|
|
525
|
+
transformed.lifecycle = event.type === 'item.started' ? 'started'
|
|
526
|
+
: event.type === 'item.completed' ? 'completed' : 'other';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add startTime for frontend timer synchronization
|
|
530
|
+
const activeSessionId = currentSessionId || provisionalSessionId;
|
|
531
|
+
const activeSession = activeSessionId ? activeCodexSessions.get(activeSessionId) : null;
|
|
532
|
+
if (Number.isFinite(activeSession?.startTime)) {
|
|
533
|
+
transformed.startTime = activeSession.startTime;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// For error/turn.failed events, send codex-error instead of codex-response
|
|
537
|
+
// to trigger the error UI with retry button (avoid sending both).
|
|
538
|
+
if (event.type === 'error' || event.type === 'turn.failed') {
|
|
539
|
+
const errorCode = event.error?.code || event.error?.type || '';
|
|
540
|
+
const errorMsg = event.error?.message || event.message || String(event.error || '');
|
|
541
|
+
const { errorType, isRetryable } = errorCode
|
|
542
|
+
? classifySDKError(errorCode, 'codex')
|
|
543
|
+
: classifyError(errorMsg);
|
|
544
|
+
sendMessage(ws, {
|
|
545
|
+
type: 'codex-error',
|
|
546
|
+
error: errorMsg || errorCode,
|
|
547
|
+
errorType,
|
|
548
|
+
isRetryable,
|
|
549
|
+
sessionId: currentSessionId,
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
sendMessage(ws, {
|
|
555
|
+
type: 'codex-response',
|
|
556
|
+
data: transformed,
|
|
557
|
+
sessionId: currentSessionId
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Extract and send token usage if available (normalized to match Claude format)
|
|
561
|
+
if (event.type === 'turn.completed' && event.usage) {
|
|
562
|
+
sendMessage(ws, {
|
|
563
|
+
type: 'token-budget',
|
|
564
|
+
data: buildCodexRealtimeTokenBudget(event.usage),
|
|
565
|
+
sessionId: currentSessionId
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const actualSessionId = thread.id || currentSessionId || provisionalSessionId;
|
|
571
|
+
|
|
572
|
+
// Send completion event immediately so the UI can settle
|
|
573
|
+
sendMessage(ws, {
|
|
574
|
+
type: 'codex-complete',
|
|
575
|
+
sessionId: currentSessionId,
|
|
576
|
+
actualSessionId
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Post-completion housekeeping — runs after the UI receives the completion signal
|
|
580
|
+
if (workingDirectory && actualSessionId) {
|
|
581
|
+
try {
|
|
582
|
+
await reconcileCodexSessionIndex(workingDirectory, {
|
|
583
|
+
sessionId: actualSessionId,
|
|
584
|
+
previousSessionId: currentSessionId,
|
|
585
|
+
projectName: encodeProjectPath(workingDirectory),
|
|
586
|
+
});
|
|
587
|
+
if (currentSessionId && actualSessionId !== currentSessionId) {
|
|
588
|
+
sessionDb.deleteSession(currentSessionId);
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.warn(`[Codex] Failed to reconcile indexed session ${actualSessionId}:`, error.message);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
} catch (error) {
|
|
596
|
+
const lookupSessionId = currentSessionId || provisionalSessionId;
|
|
597
|
+
const session = lookupSessionId ? activeCodexSessions.get(lookupSessionId) : null;
|
|
598
|
+
const wasAborted =
|
|
599
|
+
session?.status === 'aborted' ||
|
|
600
|
+
error?.name === 'AbortError' ||
|
|
601
|
+
String(error?.message || '').toLowerCase().includes('aborted');
|
|
602
|
+
|
|
603
|
+
if (!wasAborted) {
|
|
604
|
+
console.error('[Codex] Error:', error);
|
|
605
|
+
const { errorType, isRetryable } = classifyError(error.message);
|
|
606
|
+
|
|
607
|
+
sendMessage(ws, {
|
|
608
|
+
type: 'codex-error',
|
|
609
|
+
error: error.message,
|
|
610
|
+
errorType,
|
|
611
|
+
isRetryable,
|
|
612
|
+
sessionId: currentSessionId
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
} finally {
|
|
617
|
+
await cleanupCodexTempFiles(tempImagePaths, tempDir);
|
|
618
|
+
|
|
619
|
+
// Update session status
|
|
620
|
+
const finalSessionId = currentSessionId || provisionalSessionId;
|
|
621
|
+
if (finalSessionId) {
|
|
622
|
+
const session = activeCodexSessions.get(finalSessionId);
|
|
623
|
+
if (session) {
|
|
624
|
+
session.status = session.status === 'aborted' ? 'aborted' : 'completed';
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Abort an active Codex session
|
|
632
|
+
* @param {string} sessionId - Session ID to abort
|
|
633
|
+
* @returns {boolean} - Whether abort was successful
|
|
634
|
+
*/
|
|
635
|
+
export function abortCodexSession(sessionId) {
|
|
636
|
+
const session = activeCodexSessions.get(sessionId);
|
|
637
|
+
|
|
638
|
+
if (!session) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
session.status = 'aborted';
|
|
643
|
+
try {
|
|
644
|
+
session.abortController?.abort();
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check if a session is active
|
|
654
|
+
* @param {string} sessionId - Session ID to check
|
|
655
|
+
* @returns {boolean} - Whether session is active
|
|
656
|
+
*/
|
|
657
|
+
export function isCodexSessionActive(sessionId) {
|
|
658
|
+
const session = activeCodexSessions.get(sessionId);
|
|
659
|
+
return session?.status === 'running';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Get the start time of a Codex session
|
|
664
|
+
* @param {string} sessionId - Session ID
|
|
665
|
+
* @returns {number|null} Start time in ms or null
|
|
666
|
+
*/
|
|
667
|
+
export function getCodexSessionStartTime(sessionId) {
|
|
668
|
+
const session = activeCodexSessions.get(sessionId);
|
|
669
|
+
return session ? session.startTime : null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get all active sessions
|
|
674
|
+
* @returns {Array} - Array of active session info
|
|
675
|
+
*/
|
|
676
|
+
export function getActiveCodexSessions() {
|
|
677
|
+
const sessions = [];
|
|
678
|
+
|
|
679
|
+
for (const [id, session] of activeCodexSessions.entries()) {
|
|
680
|
+
if (session.status === 'running') {
|
|
681
|
+
sessions.push({
|
|
682
|
+
id,
|
|
683
|
+
status: session.status,
|
|
684
|
+
startTime: session.startTime
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return sessions;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Helper to send message via WebSocket or writer
|
|
694
|
+
* @param {WebSocket|object} ws - WebSocket or response writer
|
|
695
|
+
* @param {object} data - Data to send
|
|
696
|
+
*/
|
|
697
|
+
function sendMessage(ws, data) {
|
|
698
|
+
try {
|
|
699
|
+
if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
|
|
700
|
+
// Writer handles stringification (SSEStreamWriter or WebSocketWriter)
|
|
701
|
+
ws.send(data);
|
|
702
|
+
} else if (typeof ws.send === 'function') {
|
|
703
|
+
// Raw WebSocket - stringify here
|
|
704
|
+
ws.send(JSON.stringify(data));
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('[Codex] Error sending message:', error);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Clean up old completed sessions periodically
|
|
712
|
+
setInterval(() => {
|
|
713
|
+
const now = Date.now();
|
|
714
|
+
const maxAge = 30 * 60 * 1000; // 30 minutes
|
|
715
|
+
|
|
716
|
+
for (const [id, session] of activeCodexSessions.entries()) {
|
|
717
|
+
if (session.status !== 'running') {
|
|
718
|
+
const startTime = typeof session.startTime === 'number' ? session.startTime : Number.NaN;
|
|
719
|
+
if (Number.isFinite(startTime) && now - startTime > maxAge) {
|
|
720
|
+
activeCodexSessions.delete(id);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}, 5 * 60 * 1000); // Every 5 minutes
|