codeksei 0.1.0
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 +661 -0
- package/README.en.md +215 -0
- package/README.md +259 -0
- package/bin/codeksei.js +10 -0
- package/bin/cyberboss.js +11 -0
- package/package.json +86 -0
- package/scripts/install-background-tasks.ps1 +135 -0
- package/scripts/open_shared_wechat_thread.sh +94 -0
- package/scripts/open_wechat_thread.sh +117 -0
- package/scripts/shared-common.js +791 -0
- package/scripts/shared-open.js +46 -0
- package/scripts/shared-start.js +41 -0
- package/scripts/shared-status.js +74 -0
- package/scripts/shared-supervisor.js +141 -0
- package/scripts/shared-task-runner.ps1 +87 -0
- package/scripts/shared-watchdog.js +290 -0
- package/scripts/show_shared_status.sh +53 -0
- package/scripts/start_shared_app_server.sh +65 -0
- package/scripts/start_shared_wechat.sh +108 -0
- package/scripts/timeline-screenshot.sh +15 -0
- package/scripts/uninstall-background-tasks.ps1 +23 -0
- package/src/adapters/channel/weixin/account-store.js +135 -0
- package/src/adapters/channel/weixin/api-v2.js +258 -0
- package/src/adapters/channel/weixin/api.js +180 -0
- package/src/adapters/channel/weixin/context-token-store.js +84 -0
- package/src/adapters/channel/weixin/index.js +605 -0
- package/src/adapters/channel/weixin/legacy.js +567 -0
- package/src/adapters/channel/weixin/login-common.js +63 -0
- package/src/adapters/channel/weixin/login-legacy.js +124 -0
- package/src/adapters/channel/weixin/login-v2.js +186 -0
- package/src/adapters/channel/weixin/media-mime.js +22 -0
- package/src/adapters/channel/weixin/media-receive.js +370 -0
- package/src/adapters/channel/weixin/media-send.js +331 -0
- package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
- package/src/adapters/channel/weixin/message-utils.js +199 -0
- package/src/adapters/channel/weixin/protocol.js +77 -0
- package/src/adapters/channel/weixin/redact.js +41 -0
- package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
- package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
- package/src/adapters/runtime/codex/events.js +252 -0
- package/src/adapters/runtime/codex/index.js +502 -0
- package/src/adapters/runtime/codex/message-utils.js +141 -0
- package/src/adapters/runtime/codex/model-catalog.js +106 -0
- package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
- package/src/adapters/runtime/codex/rpc-client.js +443 -0
- package/src/adapters/runtime/codex/session-store.js +376 -0
- package/src/app/channel-send-file-cli.js +57 -0
- package/src/app/diary-write-cli.js +620 -0
- package/src/app/note-auto-cli.js +201 -0
- package/src/app/note-sync-cli.js +130 -0
- package/src/app/project-radar-cli.js +165 -0
- package/src/app/reminder-write-cli.js +210 -0
- package/src/app/review-cli.js +134 -0
- package/src/app/system-checkin-poller.js +100 -0
- package/src/app/system-send-cli.js +129 -0
- package/src/app/timeline-event-cli.js +273 -0
- package/src/app/timeline-screenshot-cli.js +109 -0
- package/src/core/app.js +1810 -0
- package/src/core/branding.js +167 -0
- package/src/core/command-registry.js +609 -0
- package/src/core/config.js +84 -0
- package/src/core/default-targets.js +163 -0
- package/src/core/durable-note-schema.js +325 -0
- package/src/core/instructions-template.js +31 -0
- package/src/core/note-sync.js +433 -0
- package/src/core/project-radar.js +402 -0
- package/src/core/review-semantic.js +524 -0
- package/src/core/review.js +1081 -0
- package/src/core/shared-bridge-heartbeat.js +140 -0
- package/src/core/stream-delivery.js +990 -0
- package/src/core/system-message-dispatcher.js +68 -0
- package/src/core/system-message-queue-store.js +128 -0
- package/src/core/thread-state-store.js +135 -0
- package/src/core/timeline-screenshot-queue-store.js +134 -0
- package/src/core/workspace-alias.js +163 -0
- package/src/core/workspace-bootstrap.js +338 -0
- package/src/index.js +270 -0
- package/src/integrations/timeline/index.js +191 -0
- package/templates/weixin-instructions.md +53 -0
- package/templates/weixin-operations.md +69 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { renderInstructionTemplate } = require("../../../core/instructions-template");
|
|
3
|
+
const { CodexRpcClient } = require("./rpc-client");
|
|
4
|
+
const { mapCodexMessageToRuntimeEvent } = require("./events");
|
|
5
|
+
const {
|
|
6
|
+
extractAssistantText,
|
|
7
|
+
extractFailureText,
|
|
8
|
+
extractThreadId,
|
|
9
|
+
extractThreadIdFromParams,
|
|
10
|
+
extractTurnIdFromParams,
|
|
11
|
+
isAssistantItemCompleted,
|
|
12
|
+
} = require("./message-utils");
|
|
13
|
+
const { SessionStore } = require("./session-store");
|
|
14
|
+
const { resolveCodexWorkspaceRoot } = require("../../../core/workspace-alias");
|
|
15
|
+
const { buildWorkspaceContinuityInstructions } = require("../../../core/workspace-bootstrap");
|
|
16
|
+
|
|
17
|
+
function createCodexRuntimeAdapter(config) {
|
|
18
|
+
const sessionStore = new SessionStore({ filePath: config.sessionsFile });
|
|
19
|
+
let client = null;
|
|
20
|
+
let readyState = null;
|
|
21
|
+
|
|
22
|
+
function ensureClient() {
|
|
23
|
+
if (!client) {
|
|
24
|
+
client = new CodexRpcClient({
|
|
25
|
+
endpoint: config.codexEndpoint,
|
|
26
|
+
codexCommand: config.codexCommand,
|
|
27
|
+
env: process.env,
|
|
28
|
+
extraWritableRoots: [config.stateDir],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return client;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isReconnectableRuntimeError(error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error || "");
|
|
36
|
+
return message.includes("Codex websocket is not connected")
|
|
37
|
+
|| message.includes("Codex websocket closed")
|
|
38
|
+
|| message.includes("Codex websocket errored")
|
|
39
|
+
|| message.includes("Codex process stdin is not writable")
|
|
40
|
+
|| message.includes("Codex RPC client closed");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function refreshReadyState(runtimeClient) {
|
|
44
|
+
const modelResponse = await runtimeClient.listModels().catch(() => null);
|
|
45
|
+
const models = Array.isArray(modelResponse?.result?.data)
|
|
46
|
+
? modelResponse.result.data
|
|
47
|
+
: [];
|
|
48
|
+
if (models.length) {
|
|
49
|
+
sessionStore.setAvailableModelCatalog(models);
|
|
50
|
+
}
|
|
51
|
+
readyState = {
|
|
52
|
+
endpoint: config.codexEndpoint || "(spawn)",
|
|
53
|
+
models,
|
|
54
|
+
};
|
|
55
|
+
return readyState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function ensureInitialized({ forceReconnect = false } = {}) {
|
|
59
|
+
const runtimeClient = ensureClient();
|
|
60
|
+
if (forceReconnect) {
|
|
61
|
+
readyState = null;
|
|
62
|
+
await runtimeClient.close();
|
|
63
|
+
}
|
|
64
|
+
if (readyState && runtimeClient.isConnected()) {
|
|
65
|
+
return readyState;
|
|
66
|
+
}
|
|
67
|
+
await runtimeClient.connect();
|
|
68
|
+
await runtimeClient.initialize();
|
|
69
|
+
return refreshReadyState(runtimeClient);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function withRuntimeReconnect(action) {
|
|
73
|
+
const runtimeClient = ensureClient();
|
|
74
|
+
try {
|
|
75
|
+
await ensureInitialized();
|
|
76
|
+
return await action(runtimeClient);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!isReconnectableRuntimeError(error)) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
await ensureInitialized({ forceReconnect: true });
|
|
82
|
+
return action(runtimeClient);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
describe() {
|
|
88
|
+
return {
|
|
89
|
+
id: "codex",
|
|
90
|
+
kind: "runtime",
|
|
91
|
+
endpoint: config.codexEndpoint || "(spawn)",
|
|
92
|
+
sessionsFile: config.sessionsFile,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
createClient() {
|
|
96
|
+
return ensureClient();
|
|
97
|
+
},
|
|
98
|
+
onEvent(listener) {
|
|
99
|
+
if (typeof listener !== "function") {
|
|
100
|
+
return () => {};
|
|
101
|
+
}
|
|
102
|
+
const runtimeClient = ensureClient();
|
|
103
|
+
return runtimeClient.onMessage((message) => {
|
|
104
|
+
const event = mapCodexMessageToRuntimeEvent(message);
|
|
105
|
+
if (event) {
|
|
106
|
+
listener(event, message);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
getSessionStore() {
|
|
111
|
+
return sessionStore;
|
|
112
|
+
},
|
|
113
|
+
async initialize() {
|
|
114
|
+
return ensureInitialized();
|
|
115
|
+
},
|
|
116
|
+
async close() {
|
|
117
|
+
if (client) {
|
|
118
|
+
await client.close();
|
|
119
|
+
}
|
|
120
|
+
readyState = null;
|
|
121
|
+
client = null;
|
|
122
|
+
},
|
|
123
|
+
async respondApproval({ requestId, decision }) {
|
|
124
|
+
return withRuntimeReconnect(async (runtimeClient) => {
|
|
125
|
+
const normalizedDecision = decision === "accept" ? "accept" : "decline";
|
|
126
|
+
if (requestId == null || String(requestId).trim() === "") {
|
|
127
|
+
throw new Error("approval response requires a requestId");
|
|
128
|
+
}
|
|
129
|
+
await runtimeClient.sendResponse(requestId, { decision: normalizedDecision });
|
|
130
|
+
return {
|
|
131
|
+
requestId,
|
|
132
|
+
decision: normalizedDecision,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
async cancelTurn({ threadId, turnId }) {
|
|
137
|
+
return withRuntimeReconnect(async (runtimeClient) => {
|
|
138
|
+
await runtimeClient.cancelTurn({ threadId, turnId });
|
|
139
|
+
return { threadId, turnId };
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
async resumeThread({ threadId }) {
|
|
143
|
+
return withRuntimeReconnect((runtimeClient) => runtimeClient.resumeThread({ threadId }));
|
|
144
|
+
},
|
|
145
|
+
async refreshThreadInstructions({ bindingKey = "", threadId, workspaceRoot, model = "", accessMode = "" }) {
|
|
146
|
+
return withRuntimeReconnect(async (runtimeClient) => {
|
|
147
|
+
const refreshText = buildInstructionRefreshText(config, workspaceRoot);
|
|
148
|
+
const runtimeWorkspaceRoot = resolveCodexWorkspaceRoot(workspaceRoot);
|
|
149
|
+
await runtimeClient.resumeThread({ threadId });
|
|
150
|
+
const completion = waitForTurnCompletion(runtimeClient, threadId);
|
|
151
|
+
await sendUserMessageWithWorkspaceDiagnostics({
|
|
152
|
+
runtimeClient,
|
|
153
|
+
params: {
|
|
154
|
+
threadId,
|
|
155
|
+
text: refreshText,
|
|
156
|
+
model,
|
|
157
|
+
accessMode,
|
|
158
|
+
workspaceRoot: runtimeWorkspaceRoot,
|
|
159
|
+
},
|
|
160
|
+
operation: "turn/start(refresh)",
|
|
161
|
+
bindingKey,
|
|
162
|
+
threadId,
|
|
163
|
+
workspaceRoot,
|
|
164
|
+
runtimeWorkspaceRoot,
|
|
165
|
+
});
|
|
166
|
+
const result = await completion;
|
|
167
|
+
if (bindingKey) {
|
|
168
|
+
sessionStore.rememberWorkspaceBootstrapForThread(bindingKey, workspaceRoot, threadId);
|
|
169
|
+
}
|
|
170
|
+
return { threadId, ...result };
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async sendTextTurn({ bindingKey, workspaceRoot, text, metadata = {}, model = "", accessMode = "" }) {
|
|
174
|
+
return withRuntimeReconnect(async (runtimeClient) => {
|
|
175
|
+
// Codex websocket metadata currently breaks on non-ASCII workspace keys.
|
|
176
|
+
// Keep session truth keyed by the canonical workspace root, but route the
|
|
177
|
+
// actual runtime cwd through the existing machine-level ASCII alias map.
|
|
178
|
+
const runtimeWorkspaceRoot = resolveCodexWorkspaceRoot(workspaceRoot);
|
|
179
|
+
|
|
180
|
+
let threadId = sessionStore.getThreadIdForWorkspace(bindingKey, workspaceRoot);
|
|
181
|
+
let outboundText = text;
|
|
182
|
+
let startedNewThread = false;
|
|
183
|
+
if (!threadId) {
|
|
184
|
+
const response = await startThreadWithWorkspaceDiagnostics({
|
|
185
|
+
runtimeClient,
|
|
186
|
+
cwd: runtimeWorkspaceRoot,
|
|
187
|
+
bindingKey,
|
|
188
|
+
workspaceRoot,
|
|
189
|
+
runtimeWorkspaceRoot,
|
|
190
|
+
});
|
|
191
|
+
threadId = extractThreadId(response);
|
|
192
|
+
if (!threadId) {
|
|
193
|
+
throw new Error("thread/start did not return a thread id");
|
|
194
|
+
}
|
|
195
|
+
sessionStore.setThreadIdForWorkspace(bindingKey, workspaceRoot, threadId, metadata);
|
|
196
|
+
startedNewThread = true;
|
|
197
|
+
} else {
|
|
198
|
+
await runtimeClient.resumeThread({ threadId }).catch(async () => {
|
|
199
|
+
sessionStore.clearThreadIdForWorkspace(bindingKey, workspaceRoot);
|
|
200
|
+
const recreated = await startThreadWithWorkspaceDiagnostics({
|
|
201
|
+
runtimeClient,
|
|
202
|
+
cwd: runtimeWorkspaceRoot,
|
|
203
|
+
bindingKey,
|
|
204
|
+
workspaceRoot,
|
|
205
|
+
runtimeWorkspaceRoot,
|
|
206
|
+
threadId,
|
|
207
|
+
});
|
|
208
|
+
threadId = extractThreadId(recreated);
|
|
209
|
+
if (!threadId) {
|
|
210
|
+
throw new Error("thread/start did not return a thread id");
|
|
211
|
+
}
|
|
212
|
+
sessionStore.setThreadIdForWorkspace(bindingKey, workspaceRoot, threadId, metadata);
|
|
213
|
+
startedNewThread = true;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const needsWorkspaceBootstrap = startedNewThread
|
|
218
|
+
|| !sessionStore.hasWorkspaceBootstrapForThread(bindingKey, workspaceRoot, threadId);
|
|
219
|
+
if (startedNewThread) {
|
|
220
|
+
outboundText = buildOpeningTurnText(config, workspaceRoot, text);
|
|
221
|
+
} else if (needsWorkspaceBootstrap) {
|
|
222
|
+
outboundText = buildWorkspaceBootstrapTurnText(config, workspaceRoot, text);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await sendUserMessageWithWorkspaceDiagnostics({
|
|
226
|
+
runtimeClient,
|
|
227
|
+
params: {
|
|
228
|
+
threadId,
|
|
229
|
+
text: outboundText,
|
|
230
|
+
model,
|
|
231
|
+
accessMode,
|
|
232
|
+
workspaceRoot: runtimeWorkspaceRoot,
|
|
233
|
+
},
|
|
234
|
+
operation: "turn/start",
|
|
235
|
+
bindingKey,
|
|
236
|
+
threadId,
|
|
237
|
+
workspaceRoot,
|
|
238
|
+
runtimeWorkspaceRoot,
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
threadId,
|
|
242
|
+
workspaceBootstrapPending: needsWorkspaceBootstrap,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildOpeningTurnText(config, workspaceRoot, userText) {
|
|
250
|
+
const instructionBlocks = buildInstructionBlocks(config, workspaceRoot);
|
|
251
|
+
const normalizedText = String(userText || "").trim();
|
|
252
|
+
if (!instructionBlocks.length) {
|
|
253
|
+
return normalizedText;
|
|
254
|
+
}
|
|
255
|
+
return [
|
|
256
|
+
...instructionBlocks,
|
|
257
|
+
"",
|
|
258
|
+
"Current user message:",
|
|
259
|
+
normalizedText,
|
|
260
|
+
].join("\n").trim();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildWorkspaceBootstrapTurnText(config, workspaceRoot, userText) {
|
|
264
|
+
const instructionBlocks = buildInstructionBlocks(config, workspaceRoot);
|
|
265
|
+
const normalizedText = String(userText || "").trim();
|
|
266
|
+
if (!instructionBlocks.length) {
|
|
267
|
+
return normalizedText;
|
|
268
|
+
}
|
|
269
|
+
return [
|
|
270
|
+
"WECHAT THREAD CONTINUITY REFRESH",
|
|
271
|
+
"This existing thread needs the current WeChat and workspace continuity context before you answer.",
|
|
272
|
+
"Keep the ongoing conversation state, but adopt the guidance below before replying.",
|
|
273
|
+
"Do not quote or summarize these instructions back to the user unless explicitly asked.",
|
|
274
|
+
"",
|
|
275
|
+
...instructionBlocks,
|
|
276
|
+
"",
|
|
277
|
+
"Current user message:",
|
|
278
|
+
normalizedText,
|
|
279
|
+
].join("\n").trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildInstructionRefreshText(config, workspaceRoot) {
|
|
283
|
+
const instructionBlocks = buildInstructionBlocks(config, workspaceRoot);
|
|
284
|
+
if (!instructionBlocks.length) {
|
|
285
|
+
return "Refresh your WeChat behavior for this existing thread. Reply in one short Chinese sentence confirming that you have updated your behavior for this thread.";
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
"WECHAT SESSION INSTRUCTIONS REFRESH",
|
|
289
|
+
"Re-read and adopt the updated WeChat and workspace continuity instructions below for the rest of this existing thread.",
|
|
290
|
+
"This is an internal refresh command, not a user-facing task.",
|
|
291
|
+
"Do not summarize the instructions back in detail.",
|
|
292
|
+
"Reply in one short Chinese sentence confirming that you have updated your behavior for this thread.",
|
|
293
|
+
"",
|
|
294
|
+
...instructionBlocks,
|
|
295
|
+
].join("\n").trim();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildInstructionBlocks(config = {}, workspaceRoot = "") {
|
|
299
|
+
const instructions = loadWechatInstructions(config);
|
|
300
|
+
const workspaceContinuity = buildWorkspaceContinuityInstructions(workspaceRoot, config);
|
|
301
|
+
const sections = [];
|
|
302
|
+
if (instructions) {
|
|
303
|
+
sections.push([
|
|
304
|
+
"WECHAT SESSION INSTRUCTIONS",
|
|
305
|
+
"These instructions define the stable behavior for this WeChat thread.",
|
|
306
|
+
"Do not quote or summarize them back to the user unless explicitly asked.",
|
|
307
|
+
"",
|
|
308
|
+
instructions,
|
|
309
|
+
].join("\n"));
|
|
310
|
+
}
|
|
311
|
+
if (workspaceContinuity) {
|
|
312
|
+
sections.push([
|
|
313
|
+
"WORKSPACE CONTINUITY",
|
|
314
|
+
workspaceContinuity,
|
|
315
|
+
].join("\n"));
|
|
316
|
+
}
|
|
317
|
+
return sections;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function loadWechatInstructions(config = {}) {
|
|
321
|
+
const persona = loadInstructionFile(config.weixinInstructionsFile, config);
|
|
322
|
+
const operations = loadInstructionFile(config.weixinOperationsFile, config);
|
|
323
|
+
return [persona, operations].filter(Boolean).join("\n\n").trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function loadInstructionFile(filePath, config = {}) {
|
|
327
|
+
const normalizedPath = typeof filePath === "string" ? filePath.trim() : "";
|
|
328
|
+
if (!normalizedPath) {
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const raw = fs.readFileSync(normalizedPath, "utf8");
|
|
333
|
+
return renderInstructionTemplate(raw, config).trim();
|
|
334
|
+
} catch {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = { createCodexRuntimeAdapter };
|
|
340
|
+
|
|
341
|
+
async function startThreadWithWorkspaceDiagnostics({
|
|
342
|
+
runtimeClient,
|
|
343
|
+
cwd,
|
|
344
|
+
bindingKey = "",
|
|
345
|
+
workspaceRoot = "",
|
|
346
|
+
runtimeWorkspaceRoot = "",
|
|
347
|
+
threadId = "",
|
|
348
|
+
}) {
|
|
349
|
+
try {
|
|
350
|
+
return await runtimeClient.startThread({ cwd });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
logInvalidWorkspaceError({
|
|
353
|
+
operation: "thread/start",
|
|
354
|
+
bindingKey,
|
|
355
|
+
threadId,
|
|
356
|
+
workspaceRoot,
|
|
357
|
+
runtimeWorkspaceRoot,
|
|
358
|
+
error,
|
|
359
|
+
});
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function sendUserMessageWithWorkspaceDiagnostics({
|
|
365
|
+
runtimeClient,
|
|
366
|
+
params,
|
|
367
|
+
operation,
|
|
368
|
+
bindingKey = "",
|
|
369
|
+
threadId = "",
|
|
370
|
+
workspaceRoot = "",
|
|
371
|
+
runtimeWorkspaceRoot = "",
|
|
372
|
+
}) {
|
|
373
|
+
try {
|
|
374
|
+
return await runtimeClient.sendUserMessage(params);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
logInvalidWorkspaceError({
|
|
377
|
+
operation,
|
|
378
|
+
bindingKey,
|
|
379
|
+
threadId: threadId || params?.threadId || "",
|
|
380
|
+
workspaceRoot,
|
|
381
|
+
runtimeWorkspaceRoot,
|
|
382
|
+
error,
|
|
383
|
+
});
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function logInvalidWorkspaceError({
|
|
389
|
+
operation,
|
|
390
|
+
bindingKey = "",
|
|
391
|
+
threadId = "",
|
|
392
|
+
workspaceRoot = "",
|
|
393
|
+
runtimeWorkspaceRoot = "",
|
|
394
|
+
error,
|
|
395
|
+
}) {
|
|
396
|
+
if (!isInvalidWorkspaceError(error)) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.error(
|
|
400
|
+
`[codeksei] codex ${operation} invalid workspace cwd `
|
|
401
|
+
+ `thread=${normalizeLogValue(threadId) || "(new)"} `
|
|
402
|
+
+ `binding=${normalizeLogValue(bindingKey) || "(none)"} `
|
|
403
|
+
+ `workspaceRoot=${normalizeLogValue(workspaceRoot) || "(empty)"} `
|
|
404
|
+
+ `workspaceState=${describeWorkspaceState(workspaceRoot)} `
|
|
405
|
+
+ `runtimeWorkspaceRoot=${normalizeLogValue(runtimeWorkspaceRoot) || "(empty)"} `
|
|
406
|
+
+ `runtimeWorkspaceState=${describeWorkspaceState(runtimeWorkspaceRoot)} `
|
|
407
|
+
+ `error=${formatErrorMessage(error)}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isInvalidWorkspaceError(error) {
|
|
412
|
+
const message = formatErrorMessage(error).toLowerCase();
|
|
413
|
+
return message.includes("os error 267")
|
|
414
|
+
|| message.includes("notadirectory")
|
|
415
|
+
|| message.includes("目录名称无效");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function describeWorkspaceState(workspaceRoot) {
|
|
419
|
+
const normalized = normalizeLogValue(workspaceRoot);
|
|
420
|
+
if (!normalized) {
|
|
421
|
+
return "empty";
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
return fs.statSync(normalized).isDirectory() ? "directory" : "not-directory";
|
|
425
|
+
} catch {
|
|
426
|
+
return "missing";
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeLogValue(value) {
|
|
431
|
+
return typeof value === "string" ? value.trim().replace(/\\/g, "/") : "";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatErrorMessage(error) {
|
|
435
|
+
return error instanceof Error ? error.message : String(error || "unknown error");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function waitForTurnCompletion(client, threadId) {
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
let activeTurnId = "";
|
|
441
|
+
const itemOrder = [];
|
|
442
|
+
const completedTextByItemId = new Map();
|
|
443
|
+
|
|
444
|
+
const cleanup = () => {
|
|
445
|
+
unsubscribe();
|
|
446
|
+
clearTimeout(timer);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const timer = setTimeout(() => {
|
|
450
|
+
cleanup();
|
|
451
|
+
reject(new Error("codex turn timed out"));
|
|
452
|
+
}, 10 * 60_000);
|
|
453
|
+
|
|
454
|
+
const unsubscribe = client.onMessage((message) => {
|
|
455
|
+
const params = message?.params || {};
|
|
456
|
+
if (extractThreadIdFromParams(params) !== threadId) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if ((message?.method === "turn/started" || message?.method === "turn/start") && !activeTurnId) {
|
|
461
|
+
activeTurnId = extractTurnIdFromParams(params);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (isAssistantItemCompleted(message)) {
|
|
466
|
+
const itemId = typeof params?.item?.id === "string" ? params.item.id.trim() : `item-${itemOrder.length + 1}`;
|
|
467
|
+
if (!completedTextByItemId.has(itemId)) {
|
|
468
|
+
itemOrder.push(itemId);
|
|
469
|
+
}
|
|
470
|
+
completedTextByItemId.set(itemId, extractAssistantText(params));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (message?.method === "turn/failed") {
|
|
475
|
+
cleanup();
|
|
476
|
+
reject(new Error(extractFailureText(params)));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (message?.method === "turn/completed") {
|
|
481
|
+
const completedTurnId = extractTurnIdFromParams(params);
|
|
482
|
+
if (activeTurnId && completedTurnId && completedTurnId !== activeTurnId) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
cleanup();
|
|
486
|
+
// A single Codex turn can emit many assistant messages: progress notes,
|
|
487
|
+
// compacted-context check-ins, then the final reply. Callers waiting for
|
|
488
|
+
// turn completion expect the latest user-facing answer, not the whole
|
|
489
|
+
// turn history concatenated together.
|
|
490
|
+
const text = itemOrder
|
|
491
|
+
.slice()
|
|
492
|
+
.reverse()
|
|
493
|
+
.map((itemId) => completedTextByItemId.get(itemId) || "")
|
|
494
|
+
.find((value) => String(value || "").trim()) || "";
|
|
495
|
+
resolve({
|
|
496
|
+
turnId: completedTurnId || activeTurnId,
|
|
497
|
+
text: String(text || "").trim() || "已完成。",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
function extractThreadId(response) {
|
|
2
|
+
return response?.result?.thread?.id || null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function extractThreadIdFromParams(params) {
|
|
6
|
+
return normalizeIdentifier(params?.threadId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function extractTurnIdFromParams(params) {
|
|
10
|
+
return normalizeIdentifier(params?.turnId || params?.turn?.id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isAssistantItemCompleted(message) {
|
|
14
|
+
return message?.method === "item/completed"
|
|
15
|
+
&& normalizeIdentifier(message?.params?.item?.type).toLowerCase() === "agentmessage";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractAssistantText(params) {
|
|
19
|
+
const directText = [
|
|
20
|
+
params?.delta,
|
|
21
|
+
params?.item?.text,
|
|
22
|
+
];
|
|
23
|
+
for (const value of directText) {
|
|
24
|
+
if (typeof value === "string" && value.length > 0) {
|
|
25
|
+
return normalizeLineEndings(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const contentObjects = [
|
|
30
|
+
params?.item?.content,
|
|
31
|
+
params?.content,
|
|
32
|
+
];
|
|
33
|
+
for (const content of contentObjects) {
|
|
34
|
+
const extracted = extractRawTextFromContent(content);
|
|
35
|
+
if (extracted) {
|
|
36
|
+
return extracted;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractAssistantPhase(params) {
|
|
44
|
+
const candidates = [
|
|
45
|
+
params?.phase,
|
|
46
|
+
params?.item?.phase,
|
|
47
|
+
params?.item?.metadata?.phase,
|
|
48
|
+
params?.metadata?.phase,
|
|
49
|
+
];
|
|
50
|
+
for (const value of candidates) {
|
|
51
|
+
const normalized = normalizeAssistantPhase(value);
|
|
52
|
+
if (normalized) {
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractFailureText(params) {
|
|
60
|
+
const rawMessage = normalizeIdentifier(params?.turn?.error?.message || params?.error?.message);
|
|
61
|
+
return rawMessage ? `执行失败:${rawMessage}` : "执行失败";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeIdentifier(value) {
|
|
65
|
+
return typeof value === "string" ? value.trim() : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeLineEndings(value) {
|
|
69
|
+
return String(value || "").replace(/\r\n/g, "\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeAssistantPhase(value) {
|
|
73
|
+
const normalized = normalizeIdentifier(value).toLowerCase();
|
|
74
|
+
if (normalized === "commentary") {
|
|
75
|
+
return "commentary";
|
|
76
|
+
}
|
|
77
|
+
// Raw Codex sessions currently emit `commentary` and `final_answer`.
|
|
78
|
+
// Keep this alias list here so stream-delivery does not fall back to fragile
|
|
79
|
+
// text heuristics just because the upstream phase label changed shape.
|
|
80
|
+
if (normalized === "final" || normalized === "final_answer") {
|
|
81
|
+
return "final";
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractRawTextFromContent(content) {
|
|
87
|
+
if (typeof content === "string" && content.length > 0) {
|
|
88
|
+
return normalizeLineEndings(content);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!content) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(content)) {
|
|
96
|
+
const parts = [];
|
|
97
|
+
for (const entry of content) {
|
|
98
|
+
if (typeof entry === "string" && entry.length > 0) {
|
|
99
|
+
parts.push(normalizeLineEndings(entry));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!entry || typeof entry !== "object") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const entryType = String(entry.type || "").toLowerCase();
|
|
106
|
+
if (entryType === "text" && typeof entry.text === "string" && entry.text.length > 0) {
|
|
107
|
+
parts.push(normalizeLineEndings(entry.text));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (typeof entry.text === "string" && entry.text.length > 0) {
|
|
111
|
+
parts.push(normalizeLineEndings(entry.text));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (typeof entry.value === "string" && entry.value.length > 0) {
|
|
115
|
+
parts.push(normalizeLineEndings(entry.value));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return parts.join("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof content !== "object") {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof content.text === "string" && content.text.length > 0) {
|
|
126
|
+
return normalizeLineEndings(content.text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
extractAssistantPhase,
|
|
134
|
+
extractAssistantText,
|
|
135
|
+
extractFailureText,
|
|
136
|
+
extractThreadId,
|
|
137
|
+
extractThreadIdFromParams,
|
|
138
|
+
extractTurnIdFromParams,
|
|
139
|
+
isAssistantItemCompleted,
|
|
140
|
+
normalizeAssistantPhase,
|
|
141
|
+
};
|