@keyou007/agent-bot 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/.env.example +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
2
|
+
import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
3
|
+
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
4
|
+
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
5
|
+
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
6
|
+
"When capturing any screenshot on Windows, use one fresh process and make it Per-Monitor DPI Aware V2 before loading System.Windows.Forms, System.Drawing, or UI Automation, and before calling any screen, window, or bounds API.",
|
|
7
|
+
"Call user32!SetProcessDpiAwarenessContext((IntPtr)-4) in that same process and verify that it succeeded or that the process is already PMv2 before continuing.",
|
|
8
|
+
"For a full monitor or desktop, query the physical monitor or virtual-desktop bounds only after PMv2 is active.",
|
|
9
|
+
"For a specific window, obtain its HWND after PMv2 is active and use DwmGetWindowAttribute with DWMWA_EXTENDED_FRAME_BOUNDS (9) for the physical visible window bounds; use GetWindowRect only as a fallback after PMv2 is active.",
|
|
10
|
+
"Never crop a window with DPI-virtualized coordinates from an unaware process or with UI Automation BoundingRectangle coordinates.",
|
|
11
|
+
"Call Graphics.CopyFromScreen with those physical coordinates and validate that the saved bitmap dimensions exactly equal the selected physical capture bounds.",
|
|
12
|
+
].join(" ");
|
|
13
|
+
const SESSION_REQUEST_TIMEOUT_MS = 30_000;
|
|
14
|
+
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
15
|
+
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
16
|
+
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
17
|
+
// Wait for its response; connection closure still rejects the request.
|
|
18
|
+
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
19
|
+
export class CodexRuntime {
|
|
20
|
+
provider;
|
|
21
|
+
logger;
|
|
22
|
+
kind = "codex";
|
|
23
|
+
sessions = new Map();
|
|
24
|
+
listeners = new Set();
|
|
25
|
+
approvals = new Map();
|
|
26
|
+
attachedClient;
|
|
27
|
+
unsubscribe;
|
|
28
|
+
unsubscribeDisconnect;
|
|
29
|
+
sessionSyncs = new Map();
|
|
30
|
+
localActivityDetector;
|
|
31
|
+
constructor(provider, logger) {
|
|
32
|
+
this.provider = provider;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
const codexHome = provider.getCodexHome?.();
|
|
35
|
+
if (codexHome)
|
|
36
|
+
this.localActivityDetector = new CodexLocalActivityDetector(codexHome);
|
|
37
|
+
this.unsubscribeDisconnect = provider.onDisconnect?.((error) => this.handleDisconnect(error));
|
|
38
|
+
}
|
|
39
|
+
getSession(localSessionId) {
|
|
40
|
+
return this.sessions.get(localSessionId);
|
|
41
|
+
}
|
|
42
|
+
async createSession(input) {
|
|
43
|
+
const client = await this.client();
|
|
44
|
+
const response = await client.request("thread/start", {
|
|
45
|
+
cwd: input.cwd,
|
|
46
|
+
model: input.model,
|
|
47
|
+
threadSource: "user",
|
|
48
|
+
...threadLifecycleParams(input.cwd),
|
|
49
|
+
...permissionParams(input.permissionMode),
|
|
50
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
51
|
+
const requestedTitle = normalizeTaskTitle(input.title);
|
|
52
|
+
if (requestedTitle) {
|
|
53
|
+
await client.request("thread/name/set", {
|
|
54
|
+
threadId: response.thread.id,
|
|
55
|
+
name: requestedTitle,
|
|
56
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
57
|
+
}
|
|
58
|
+
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
59
|
+
const session = this.makeSession(input, response, reasoningEffort);
|
|
60
|
+
if (requestedTitle)
|
|
61
|
+
session.title = requestedTitle;
|
|
62
|
+
this.sessions.set(input.localSessionId, session);
|
|
63
|
+
return session;
|
|
64
|
+
}
|
|
65
|
+
async resumeSession(input) {
|
|
66
|
+
const client = await this.client();
|
|
67
|
+
const response = await client.request("thread/resume", {
|
|
68
|
+
threadId: input.remoteSessionId,
|
|
69
|
+
cwd: input.cwd,
|
|
70
|
+
model: input.model,
|
|
71
|
+
...threadLifecycleParams(input.cwd),
|
|
72
|
+
...permissionParams(input.permissionMode),
|
|
73
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
74
|
+
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
75
|
+
const session = this.makeSession(input, response, reasoningEffort);
|
|
76
|
+
this.sessions.set(input.localSessionId, session);
|
|
77
|
+
return session;
|
|
78
|
+
}
|
|
79
|
+
async forkSession(input) {
|
|
80
|
+
const client = await this.client();
|
|
81
|
+
const response = await client.request("thread/fork", {
|
|
82
|
+
threadId: input.remoteSessionId,
|
|
83
|
+
lastTurnId: input.lastTurnId,
|
|
84
|
+
cwd: input.cwd,
|
|
85
|
+
model: input.model,
|
|
86
|
+
threadSource: "user",
|
|
87
|
+
...threadLifecycleParams(input.cwd),
|
|
88
|
+
...permissionParams(input.permissionMode),
|
|
89
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
90
|
+
const requestedTitle = normalizeTaskTitle(input.title);
|
|
91
|
+
if (requestedTitle) {
|
|
92
|
+
await client.request("thread/name/set", {
|
|
93
|
+
threadId: response.thread.id,
|
|
94
|
+
name: requestedTitle,
|
|
95
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
96
|
+
}
|
|
97
|
+
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
98
|
+
const session = this.makeSession(input, response, reasoningEffort);
|
|
99
|
+
if (requestedTitle)
|
|
100
|
+
session.title = requestedTitle;
|
|
101
|
+
this.sessions.set(input.localSessionId, session);
|
|
102
|
+
return session;
|
|
103
|
+
}
|
|
104
|
+
async startTurn(sessionId, prompt) {
|
|
105
|
+
const session = this.requireSession(sessionId);
|
|
106
|
+
const client = await this.client();
|
|
107
|
+
await this.ensureSessionResumed(session, client);
|
|
108
|
+
const response = await client.request("turn/start", {
|
|
109
|
+
threadId: session.remoteSessionId,
|
|
110
|
+
input: codexUserInput(prompt),
|
|
111
|
+
cwd: session.cwd,
|
|
112
|
+
model: session.model,
|
|
113
|
+
effort: session.reasoningEffort,
|
|
114
|
+
summary: "auto",
|
|
115
|
+
approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
|
|
116
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
117
|
+
if (session.activeTurnId !== response.turn.id) {
|
|
118
|
+
this.adoptTurn(session, response.turn.id, Date.now());
|
|
119
|
+
}
|
|
120
|
+
return response.turn.id;
|
|
121
|
+
}
|
|
122
|
+
async readSessionMetadata(remoteSessionId) {
|
|
123
|
+
const response = await (await this.client()).request("thread/read", { threadId: remoteSessionId, includeTurns: false }, 5_000);
|
|
124
|
+
return {
|
|
125
|
+
title: normalizeTaskTitle(response.thread.name) ?? normalizeTaskTitle(response.thread.preview),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async listRemoteSessions(input = {}) {
|
|
129
|
+
const client = await this.client();
|
|
130
|
+
const response = await client.request("thread/list", {
|
|
131
|
+
cursor: input.cursor,
|
|
132
|
+
limit: input.limit ?? 20,
|
|
133
|
+
sortKey: "recency_at",
|
|
134
|
+
sortDirection: "desc",
|
|
135
|
+
archived: false,
|
|
136
|
+
searchTerm: input.searchTerm,
|
|
137
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
138
|
+
const sessions = await Promise.all(response.data.map(async (thread) => {
|
|
139
|
+
const listed = remoteSessionSummary(thread);
|
|
140
|
+
if (listed.status === "active" || listed.lastTurnStatus === "inProgress")
|
|
141
|
+
return listed;
|
|
142
|
+
try {
|
|
143
|
+
const detail = await client.request("thread/read", { threadId: thread.id, includeTurns: true }, SYNC_REQUEST_TIMEOUT_MS);
|
|
144
|
+
return mergeRemoteSessionSummary(listed, remoteSessionSummary(detail.thread));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return listed;
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
const activeThreads = await this.localActivityDetector?.activeThreads(sessions.map((session) => session.id));
|
|
151
|
+
return {
|
|
152
|
+
sessions: activeThreads
|
|
153
|
+
? sessions.map((session) => markLocallyDetectedActive(session, activeThreads))
|
|
154
|
+
: sessions,
|
|
155
|
+
nextCursor: response.nextCursor ?? undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async readRemoteSession(remoteSessionId) {
|
|
159
|
+
const response = await (await this.client()).request("thread/read", { threadId: remoteSessionId, includeTurns: true }, SYNC_REQUEST_TIMEOUT_MS);
|
|
160
|
+
const summary = remoteSessionSummary(response.thread);
|
|
161
|
+
const activeThreads = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
162
|
+
const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
|
|
163
|
+
const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
|
|
164
|
+
return {
|
|
165
|
+
...activeSummary,
|
|
166
|
+
...settings?.get(remoteSessionId),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async inspectRemoteSessionActivity(remoteSessionId) {
|
|
170
|
+
const [response, activeThreads] = await Promise.all([
|
|
171
|
+
(await this.client()).request("thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS),
|
|
172
|
+
this.localActivityDetector?.activeThreads([remoteSessionId]),
|
|
173
|
+
]);
|
|
174
|
+
const runtimeSession = [...this.sessions.values()]
|
|
175
|
+
.find((session) => session.remoteSessionId === remoteSessionId);
|
|
176
|
+
const detectedTurnId = activeThreads?.get(remoteSessionId);
|
|
177
|
+
const active = remoteThreadStatus(response.thread.status?.type) === "active"
|
|
178
|
+
|| Boolean(runtimeSession?.activeTurnId)
|
|
179
|
+
|| Boolean(activeThreads?.has(remoteSessionId));
|
|
180
|
+
const activeTurnId = detectedTurnId ?? runtimeSession?.activeTurnId;
|
|
181
|
+
return {
|
|
182
|
+
active,
|
|
183
|
+
...(activeTurnId ? { activeTurnId } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async synchronizeSession(sessionId) {
|
|
187
|
+
const existing = this.sessionSyncs.get(sessionId);
|
|
188
|
+
if (existing)
|
|
189
|
+
return existing;
|
|
190
|
+
const synchronization = this.synchronizeSessionNow(sessionId);
|
|
191
|
+
this.sessionSyncs.set(sessionId, synchronization);
|
|
192
|
+
try {
|
|
193
|
+
return await synchronization;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (this.sessionSyncs.get(sessionId) === synchronization)
|
|
197
|
+
this.sessionSyncs.delete(sessionId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async steerTurn(sessionId, turnId, prompt) {
|
|
201
|
+
const session = this.requireActiveTurn(sessionId, turnId);
|
|
202
|
+
await (await this.client()).request("turn/steer", {
|
|
203
|
+
threadId: session.remoteSessionId,
|
|
204
|
+
expectedTurnId: turnId,
|
|
205
|
+
input: codexUserInput(prompt),
|
|
206
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
207
|
+
}
|
|
208
|
+
async cancelTurn(sessionId, turnId) {
|
|
209
|
+
const session = this.requireActiveTurn(sessionId, turnId);
|
|
210
|
+
await this.interruptRemoteTurn(session.remoteSessionId, turnId, sessionId);
|
|
211
|
+
}
|
|
212
|
+
async interruptRemoteTurn(remoteSessionId, turnId, sessionId) {
|
|
213
|
+
await (await this.client()).request("turn/interrupt", { threadId: remoteSessionId, turnId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
214
|
+
this.logger.info({ ...(sessionId ? { sessionId } : {}), threadId: remoteSessionId, turnId }, "Codex accepted the turn interrupt request.");
|
|
215
|
+
}
|
|
216
|
+
async closeSession(sessionId) {
|
|
217
|
+
const session = this.requireSession(sessionId);
|
|
218
|
+
await (await this.client()).request("thread/archive", { threadId: session.remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
219
|
+
this.sessions.delete(sessionId);
|
|
220
|
+
}
|
|
221
|
+
async setTitle(sessionId, title) {
|
|
222
|
+
const session = this.requireSession(sessionId);
|
|
223
|
+
const normalizedTitle = normalizeTaskTitle(title);
|
|
224
|
+
if (!normalizedTitle)
|
|
225
|
+
throw new Error("任务标题不能为空。");
|
|
226
|
+
await (await this.client()).request("thread/name/set", { threadId: session.remoteSessionId, name: normalizedTitle }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
227
|
+
session.title = normalizedTitle;
|
|
228
|
+
}
|
|
229
|
+
async getGoal(sessionId) {
|
|
230
|
+
const session = this.requireSession(sessionId);
|
|
231
|
+
const client = await this.client();
|
|
232
|
+
await this.ensureSessionResumed(session, client);
|
|
233
|
+
const response = await client.request("thread/goal/get", { threadId: session.remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
234
|
+
return response.goal ?? undefined;
|
|
235
|
+
}
|
|
236
|
+
async setGoal(sessionId, update) {
|
|
237
|
+
const session = this.requireSession(sessionId);
|
|
238
|
+
const client = await this.client();
|
|
239
|
+
await this.ensureSessionResumed(session, client);
|
|
240
|
+
const response = await client.request("thread/goal/set", { threadId: session.remoteSessionId, ...update }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
241
|
+
return response.goal;
|
|
242
|
+
}
|
|
243
|
+
async clearGoal(sessionId) {
|
|
244
|
+
const session = this.requireSession(sessionId);
|
|
245
|
+
const client = await this.client();
|
|
246
|
+
await this.ensureSessionResumed(session, client);
|
|
247
|
+
const response = await client.request("thread/goal/clear", { threadId: session.remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
248
|
+
return response.cleared;
|
|
249
|
+
}
|
|
250
|
+
async setModel(sessionId, model) {
|
|
251
|
+
this.requireSession(sessionId).model = model;
|
|
252
|
+
}
|
|
253
|
+
async setReasoningEffort(sessionId, effort) {
|
|
254
|
+
this.requireSession(sessionId).reasoningEffort = effort;
|
|
255
|
+
}
|
|
256
|
+
async setPermissionMode(sessionId, mode) {
|
|
257
|
+
this.requireSession(sessionId).permissionMode = mode;
|
|
258
|
+
}
|
|
259
|
+
async respondToApproval(sessionId, requestId, decision) {
|
|
260
|
+
const pending = this.approvals.get(requestId);
|
|
261
|
+
if (!pending || pending.sessionId !== sessionId)
|
|
262
|
+
throw new Error("Approval request is no longer pending.");
|
|
263
|
+
this.approvals.delete(requestId);
|
|
264
|
+
pending.resolve({ decision });
|
|
265
|
+
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
266
|
+
}
|
|
267
|
+
async listModels() {
|
|
268
|
+
const response = await (await this.client()).request("model/list", {});
|
|
269
|
+
return response.data.filter((model) => model.id).map((model) => ({
|
|
270
|
+
id: model.id,
|
|
271
|
+
displayName: model.displayName,
|
|
272
|
+
isDefault: model.isDefault,
|
|
273
|
+
supportedReasoningEfforts: (model.supportedReasoningEfforts ?? []).map((option) => ({
|
|
274
|
+
value: option.reasoningEffort,
|
|
275
|
+
description: option.description,
|
|
276
|
+
})),
|
|
277
|
+
defaultReasoningEffort: model.defaultReasoningEffort,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
onEvent(listener) {
|
|
281
|
+
this.listeners.add(listener);
|
|
282
|
+
return () => this.listeners.delete(listener);
|
|
283
|
+
}
|
|
284
|
+
close() {
|
|
285
|
+
this.unsubscribe?.();
|
|
286
|
+
this.unsubscribeDisconnect?.();
|
|
287
|
+
for (const pending of this.approvals.values())
|
|
288
|
+
pending.resolve({ decision: "cancel" });
|
|
289
|
+
this.approvals.clear();
|
|
290
|
+
this.sessionSyncs.clear();
|
|
291
|
+
this.sessions.clear();
|
|
292
|
+
this.provider.close();
|
|
293
|
+
}
|
|
294
|
+
async client() {
|
|
295
|
+
const client = await this.provider.getClient();
|
|
296
|
+
if (client !== this.attachedClient)
|
|
297
|
+
this.attachClient(client);
|
|
298
|
+
return client;
|
|
299
|
+
}
|
|
300
|
+
attachClient(client) {
|
|
301
|
+
this.unsubscribe?.();
|
|
302
|
+
this.attachedClient = client;
|
|
303
|
+
this.unsubscribe = client.onNotification((method, params) => this.handleNotification(method, params));
|
|
304
|
+
for (const method of [
|
|
305
|
+
"item/commandExecution/requestApproval",
|
|
306
|
+
"item/fileChange/requestApproval",
|
|
307
|
+
"item/permissions/requestApproval",
|
|
308
|
+
]) {
|
|
309
|
+
client.registerRequestHandler(method, (params, id) => this.handleApproval(params, id));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
handleNotification(method, params) {
|
|
313
|
+
if (method === "thread/name/updated" && isRecord(params)) {
|
|
314
|
+
const threadId = stringValue(params.threadId);
|
|
315
|
+
const title = normalizeTaskTitle(stringValue(params.threadName));
|
|
316
|
+
if (!threadId || !title)
|
|
317
|
+
return;
|
|
318
|
+
const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === threadId);
|
|
319
|
+
if (!session)
|
|
320
|
+
return;
|
|
321
|
+
session.title = title;
|
|
322
|
+
this.emit({ type: "session_metadata_updated", sessionId: session.localSessionId, title });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (method === "thread/status/changed" && isRecord(params)) {
|
|
326
|
+
const threadId = stringValue(params.threadId);
|
|
327
|
+
const status = isRecord(params.status) ? stringValue(params.status.type) : undefined;
|
|
328
|
+
const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === threadId);
|
|
329
|
+
if (session?.activeTurnId && status && status !== "active") {
|
|
330
|
+
void this.synchronizeSession(session.localSessionId).catch((error) => {
|
|
331
|
+
this.logger.warn({ error, sessionId: session.localSessionId, status }, "Failed to reconcile Codex thread status.");
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const mapped = mapCodexNotification(method, params);
|
|
337
|
+
if (!mapped)
|
|
338
|
+
return;
|
|
339
|
+
const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === mapped.threadId);
|
|
340
|
+
if (!session)
|
|
341
|
+
return;
|
|
342
|
+
if (session.terminalTurnIds.has(mapped.turnId)) {
|
|
343
|
+
this.logger.debug({ sessionId: session.localSessionId, turnId: mapped.turnId, method }, "Ignoring a replayed terminal Codex turn event.");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (mapped.kind === "turn_started") {
|
|
347
|
+
if (session.activeTurnId === mapped.turnId)
|
|
348
|
+
return;
|
|
349
|
+
if (session.activeTurnId
|
|
350
|
+
&& session.activeTurnStartedAt !== undefined
|
|
351
|
+
&& mapped.startedAt !== undefined
|
|
352
|
+
&& mapped.startedAt < session.activeTurnStartedAt) {
|
|
353
|
+
this.logger.debug({
|
|
354
|
+
sessionId: session.localSessionId,
|
|
355
|
+
activeTurnId: session.activeTurnId,
|
|
356
|
+
activeTurnStartedAt: session.activeTurnStartedAt,
|
|
357
|
+
notificationTurnId: mapped.turnId,
|
|
358
|
+
notificationStartedAt: mapped.startedAt,
|
|
359
|
+
}, "Ignoring a replayed historical Codex turn start.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (session.activeTurnId)
|
|
363
|
+
this.supersedeTurn(session, session.activeTurnId);
|
|
364
|
+
this.adoptTurn(session, mapped.turnId, mapped.startedAt ?? Date.now());
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!session.activeTurnId || session.activeTurnId !== mapped.turnId) {
|
|
368
|
+
this.logger.debug({ sessionId: session.localSessionId, activeTurnId: session.activeTurnId, notificationTurnId: mapped.turnId, method }, "Ignoring out-of-order Codex notification and scheduling reconciliation.");
|
|
369
|
+
void this.synchronizeSession(session.localSessionId).catch((error) => {
|
|
370
|
+
this.logger.warn({ error, sessionId: session.localSessionId }, "Failed to reconcile out-of-order Codex notification.");
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const sessionId = session.localSessionId;
|
|
375
|
+
if (mapped.kind === "token_usage") {
|
|
376
|
+
this.emit({
|
|
377
|
+
type: "token_usage_updated",
|
|
378
|
+
sessionId,
|
|
379
|
+
turnId: mapped.turnId,
|
|
380
|
+
lastTokens: mapped.lastTokens,
|
|
381
|
+
cumulativeTokens: mapped.cumulativeTokens,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else if (mapped.kind === "agent_message_phase") {
|
|
385
|
+
session.messagePhases.set(mapped.itemId, mapped.phase);
|
|
386
|
+
}
|
|
387
|
+
else if (mapped.kind === "agent_delta") {
|
|
388
|
+
if (session.messagePhases.get(mapped.itemId) === "commentary") {
|
|
389
|
+
this.emit({
|
|
390
|
+
type: "progress",
|
|
391
|
+
sessionId,
|
|
392
|
+
turnId: mapped.turnId,
|
|
393
|
+
activityId: `commentary:${mapped.itemId}`,
|
|
394
|
+
text: mapped.text,
|
|
395
|
+
append: true,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
session.finalText += mapped.text;
|
|
400
|
+
this.emit({ type: "agent_text_delta", sessionId, turnId: mapped.turnId, text: mapped.text });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else if (mapped.kind === "progress") {
|
|
404
|
+
this.emit({
|
|
405
|
+
type: "progress",
|
|
406
|
+
sessionId,
|
|
407
|
+
turnId: mapped.turnId,
|
|
408
|
+
activityId: mapped.activityId,
|
|
409
|
+
text: mapped.text,
|
|
410
|
+
append: mapped.append,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else if (mapped.kind === "plan") {
|
|
414
|
+
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
415
|
+
}
|
|
416
|
+
else if (mapped.kind === "tool") {
|
|
417
|
+
this.emit({
|
|
418
|
+
type: mapped.phase === "started" ? "tool_started" : "tool_updated",
|
|
419
|
+
sessionId,
|
|
420
|
+
turnId: mapped.turnId,
|
|
421
|
+
tool: mapped.tool,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else if (mapped.kind === "tool_output_delta") {
|
|
425
|
+
this.emit({
|
|
426
|
+
type: "tool_output_delta",
|
|
427
|
+
sessionId,
|
|
428
|
+
turnId: mapped.turnId,
|
|
429
|
+
toolId: mapped.toolId,
|
|
430
|
+
delta: mapped.delta,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else if (mapped.kind === "terminal") {
|
|
434
|
+
session.activeTurnId = undefined;
|
|
435
|
+
session.activeTurnStartedAt = undefined;
|
|
436
|
+
session.terminalTurnIds.add(mapped.turnId);
|
|
437
|
+
session.messagePhases.clear();
|
|
438
|
+
if (mapped.status === "cancelled") {
|
|
439
|
+
this.emit({ type: "turn_cancelled", sessionId, turnId: mapped.turnId });
|
|
440
|
+
}
|
|
441
|
+
else if (mapped.status === "failed") {
|
|
442
|
+
this.emit({ type: "turn_failed", sessionId, turnId: mapped.turnId, message: mapped.error ?? "Codex turn failed." });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
this.emit({
|
|
446
|
+
type: "turn_completed",
|
|
447
|
+
sessionId,
|
|
448
|
+
turnId: mapped.turnId,
|
|
449
|
+
finalResponse: session.finalText,
|
|
450
|
+
durationMs: mapped.durationMs,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
handleApproval(params, id) {
|
|
456
|
+
if (!isRecord(params))
|
|
457
|
+
return Promise.resolve({ decision: "decline" });
|
|
458
|
+
const threadId = stringValue(params.threadId);
|
|
459
|
+
const turnId = stringValue(params.turnId);
|
|
460
|
+
const session = [...this.sessions.values()].find((candidate) => candidate.remoteSessionId === threadId);
|
|
461
|
+
if (!session || !turnId)
|
|
462
|
+
return Promise.resolve({ decision: "decline" });
|
|
463
|
+
if (session.permissionMode === "auto")
|
|
464
|
+
return Promise.resolve({ decision: "accept" });
|
|
465
|
+
const requestId = String(id);
|
|
466
|
+
const response = new Promise((resolve) => {
|
|
467
|
+
this.approvals.set(requestId, { sessionId: session.localSessionId, turnId, resolve });
|
|
468
|
+
});
|
|
469
|
+
this.emit({
|
|
470
|
+
type: "approval_requested",
|
|
471
|
+
sessionId: session.localSessionId,
|
|
472
|
+
turnId,
|
|
473
|
+
request: {
|
|
474
|
+
id: requestId,
|
|
475
|
+
title: stringValue(params.command) ?? "Codex approval request",
|
|
476
|
+
command: stringValue(params.command),
|
|
477
|
+
reason: stringValue(params.reason),
|
|
478
|
+
options: [
|
|
479
|
+
{ id: "accept", label: "允许一次" },
|
|
480
|
+
{ id: "acceptForSession", label: "本会话允许" },
|
|
481
|
+
{ id: "decline", label: "拒绝" },
|
|
482
|
+
{ id: "cancel", label: "取消任务" },
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
return response;
|
|
487
|
+
}
|
|
488
|
+
async resolveReasoningEffort(input, response) {
|
|
489
|
+
if (input.reasoningEffort)
|
|
490
|
+
return input.reasoningEffort;
|
|
491
|
+
if (response.reasoningEffort)
|
|
492
|
+
return response.reasoningEffort;
|
|
493
|
+
const model = input.model ?? response.model;
|
|
494
|
+
const models = await this.listModels();
|
|
495
|
+
return models.find((item) => item.id === model)?.defaultReasoningEffort
|
|
496
|
+
?? models.find((item) => item.isDefault)?.defaultReasoningEffort;
|
|
497
|
+
}
|
|
498
|
+
async ensureSessionResumed(session, client) {
|
|
499
|
+
if (!session.needsResume)
|
|
500
|
+
return;
|
|
501
|
+
await client.request("thread/resume", {
|
|
502
|
+
threadId: session.remoteSessionId,
|
|
503
|
+
cwd: session.cwd,
|
|
504
|
+
model: session.model,
|
|
505
|
+
...threadLifecycleParams(session.cwd),
|
|
506
|
+
...permissionParams(session.permissionMode),
|
|
507
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
508
|
+
session.needsResume = false;
|
|
509
|
+
}
|
|
510
|
+
async synchronizeSessionNow(sessionId) {
|
|
511
|
+
const session = this.requireSession(sessionId);
|
|
512
|
+
const response = await (await this.client()).request("thread/read", { threadId: session.remoteSessionId, includeTurns: true }, SYNC_REQUEST_TIMEOUT_MS);
|
|
513
|
+
this.reconcileThreadSnapshot(session, response.thread);
|
|
514
|
+
return session;
|
|
515
|
+
}
|
|
516
|
+
reconcileThreadSnapshot(session, thread) {
|
|
517
|
+
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
518
|
+
const latest = turns.at(-1);
|
|
519
|
+
const latestInProgress = [...turns].reverse().find((turn) => turn.status === "inProgress");
|
|
520
|
+
const activeTurnId = session.activeTurnId;
|
|
521
|
+
if (!activeTurnId) {
|
|
522
|
+
if (latestInProgress)
|
|
523
|
+
this.adoptTurn(session, latestInProgress.id, turnStartedAt(latestInProgress));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (latestInProgress?.id === activeTurnId)
|
|
527
|
+
return;
|
|
528
|
+
if (latestInProgress) {
|
|
529
|
+
this.supersedeTurn(session, activeTurnId);
|
|
530
|
+
this.adoptTurn(session, latestInProgress.id, turnStartedAt(latestInProgress));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!latest) {
|
|
534
|
+
if (thread.status?.type !== "active") {
|
|
535
|
+
session.activeTurnId = undefined;
|
|
536
|
+
session.activeTurnStartedAt = undefined;
|
|
537
|
+
session.messagePhases.clear();
|
|
538
|
+
this.emit({
|
|
539
|
+
type: "turn_failed",
|
|
540
|
+
sessionId: session.localSessionId,
|
|
541
|
+
turnId: activeTurnId,
|
|
542
|
+
message: "Codex no longer reports this execution in the thread history.",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (latest.id !== activeTurnId) {
|
|
548
|
+
this.supersedeTurn(session, activeTurnId);
|
|
549
|
+
this.adoptTurn(session, latest.id, turnStartedAt(latest));
|
|
550
|
+
}
|
|
551
|
+
this.finishSnapshotTurn(session, latest);
|
|
552
|
+
}
|
|
553
|
+
adoptTurn(session, turnId, startedAt) {
|
|
554
|
+
session.activeTurnId = turnId;
|
|
555
|
+
session.activeTurnStartedAt = startedAt;
|
|
556
|
+
session.finalText = "";
|
|
557
|
+
session.messagePhases.clear();
|
|
558
|
+
this.emit({ type: "turn_started", sessionId: session.localSessionId, turnId, startedAt });
|
|
559
|
+
}
|
|
560
|
+
supersedeTurn(session, turnId) {
|
|
561
|
+
if (session.activeTurnId === turnId) {
|
|
562
|
+
session.activeTurnId = undefined;
|
|
563
|
+
session.activeTurnStartedAt = undefined;
|
|
564
|
+
}
|
|
565
|
+
session.terminalTurnIds.add(turnId);
|
|
566
|
+
session.messagePhases.clear();
|
|
567
|
+
this.emit({ type: "turn_cancelled", sessionId: session.localSessionId, turnId });
|
|
568
|
+
}
|
|
569
|
+
finishSnapshotTurn(session, turn) {
|
|
570
|
+
if (session.activeTurnId !== turn.id)
|
|
571
|
+
return;
|
|
572
|
+
session.activeTurnId = undefined;
|
|
573
|
+
session.activeTurnStartedAt = undefined;
|
|
574
|
+
session.terminalTurnIds.add(turn.id);
|
|
575
|
+
session.messagePhases.clear();
|
|
576
|
+
if (turn.status === "interrupted") {
|
|
577
|
+
this.emit({ type: "turn_cancelled", sessionId: session.localSessionId, turnId: turn.id });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (turn.status === "failed") {
|
|
581
|
+
this.emit({
|
|
582
|
+
type: "turn_failed",
|
|
583
|
+
sessionId: session.localSessionId,
|
|
584
|
+
turnId: turn.id,
|
|
585
|
+
message: turn.error?.message ?? "Codex turn failed.",
|
|
586
|
+
});
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const finalResponse = extractFinalResponse(turn) || session.finalText;
|
|
590
|
+
session.finalText = finalResponse;
|
|
591
|
+
this.emit({
|
|
592
|
+
type: "turn_completed",
|
|
593
|
+
sessionId: session.localSessionId,
|
|
594
|
+
turnId: turn.id,
|
|
595
|
+
finalResponse,
|
|
596
|
+
durationMs: turn.durationMs ?? undefined,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
makeSession(input, response, reasoningEffort) {
|
|
600
|
+
return {
|
|
601
|
+
localSessionId: input.localSessionId,
|
|
602
|
+
remoteSessionId: response.thread.id,
|
|
603
|
+
runtimeKind: "codex",
|
|
604
|
+
agentName: input.agentName,
|
|
605
|
+
cwd: input.cwd,
|
|
606
|
+
title: normalizeTaskTitle(response.thread.name)
|
|
607
|
+
?? normalizeTaskTitle(response.thread.preview)
|
|
608
|
+
?? input.title,
|
|
609
|
+
model: input.model ?? response.model,
|
|
610
|
+
reasoningEffort,
|
|
611
|
+
permissionMode: input.permissionMode,
|
|
612
|
+
activeTurnId: "activeTurnId" in input ? input.activeTurnId : undefined,
|
|
613
|
+
activeTurnStartedAt: undefined,
|
|
614
|
+
terminalTurnIds: new Set("lastTurnStatus" in input
|
|
615
|
+
&& input.lastTurnId
|
|
616
|
+
&& input.lastTurnStatus
|
|
617
|
+
&& input.lastTurnStatus !== "running"
|
|
618
|
+
? [input.lastTurnId]
|
|
619
|
+
: []),
|
|
620
|
+
finalText: "",
|
|
621
|
+
messagePhases: new Map(),
|
|
622
|
+
needsResume: false,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
requireSession(sessionId) {
|
|
626
|
+
const session = this.sessions.get(sessionId);
|
|
627
|
+
if (!session)
|
|
628
|
+
throw new Error(`Unknown Codex session: ${sessionId}`);
|
|
629
|
+
return session;
|
|
630
|
+
}
|
|
631
|
+
requireActiveTurn(sessionId, turnId) {
|
|
632
|
+
const session = this.requireSession(sessionId);
|
|
633
|
+
if (session.activeTurnId !== turnId)
|
|
634
|
+
throw new Error(`Codex turn is no longer active: ${turnId}`);
|
|
635
|
+
return session;
|
|
636
|
+
}
|
|
637
|
+
emit(event) {
|
|
638
|
+
for (const listener of this.listeners)
|
|
639
|
+
listener(event);
|
|
640
|
+
}
|
|
641
|
+
handleDisconnect(error) {
|
|
642
|
+
this.unsubscribe?.();
|
|
643
|
+
this.unsubscribe = undefined;
|
|
644
|
+
this.attachedClient = undefined;
|
|
645
|
+
for (const session of this.sessions.values()) {
|
|
646
|
+
session.needsResume = true;
|
|
647
|
+
const turnId = session.activeTurnId;
|
|
648
|
+
if (!turnId)
|
|
649
|
+
continue;
|
|
650
|
+
session.activeTurnId = undefined;
|
|
651
|
+
session.activeTurnStartedAt = undefined;
|
|
652
|
+
this.emit({
|
|
653
|
+
type: "turn_failed",
|
|
654
|
+
sessionId: session.localSessionId,
|
|
655
|
+
turnId,
|
|
656
|
+
message: `Codex App Server disconnected: ${error.message}`,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
for (const [requestId, pending] of this.approvals) {
|
|
660
|
+
this.approvals.delete(requestId);
|
|
661
|
+
pending.resolve({ decision: "cancel" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function permissionParams(mode) {
|
|
666
|
+
return mode === "auto"
|
|
667
|
+
? { approvalPolicy: "never", sandbox: "danger-full-access" }
|
|
668
|
+
: { approvalPolicy: "on-request", sandbox: "workspace-write" };
|
|
669
|
+
}
|
|
670
|
+
function threadLifecycleParams(cwd) {
|
|
671
|
+
const projectless = detectProjectlessWorkspace(cwd);
|
|
672
|
+
if (!projectless)
|
|
673
|
+
return { developerInstructions: WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS };
|
|
674
|
+
const projectlessInstructions = [
|
|
675
|
+
"### Projectless Chat",
|
|
676
|
+
"This projectless thread starts in a generated directory under the user's Documents/Codex folder.",
|
|
677
|
+
"Prefer answering inline in chat unless using local files would make the result more useful.",
|
|
678
|
+
`Use work/ for intermediate files, scratch analysis, scripts, drafts, and temporary assets. Use ${projectless.outputDirectory} only for user-facing deliverables that should appear as outputs.`,
|
|
679
|
+
`When referring to saved deliverables in the final response, link only files from ${projectless.outputDirectory}.`,
|
|
680
|
+
"Do not write directly in the home directory unless the user explicitly asks.",
|
|
681
|
+
].join("\n");
|
|
682
|
+
return {
|
|
683
|
+
developerInstructions: `${WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS}\n\n${projectlessInstructions}`,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function isRecord(value) {
|
|
687
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
688
|
+
}
|
|
689
|
+
function stringValue(value) {
|
|
690
|
+
return typeof value === "string" ? value : undefined;
|
|
691
|
+
}
|
|
692
|
+
function turnStartedAt(turn) {
|
|
693
|
+
return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
|
|
694
|
+
}
|
|
695
|
+
function extractFinalResponse(turn) {
|
|
696
|
+
const messages = (turn.items ?? []).filter((item) => item.type === "agentMessage" && typeof item.text === "string" && item.text.trim());
|
|
697
|
+
const finalMessages = messages.filter((item) => item.phase === "final_answer");
|
|
698
|
+
return (finalMessages.length ? finalMessages : messages.slice(-1))
|
|
699
|
+
.map((item) => item.text.trim())
|
|
700
|
+
.join("\n\n");
|
|
701
|
+
}
|
|
702
|
+
function remoteSessionSummary(thread) {
|
|
703
|
+
const lastTurn = thread.turns?.at(-1);
|
|
704
|
+
const lastCompletedTurn = [...(thread.turns ?? [])]
|
|
705
|
+
.reverse()
|
|
706
|
+
.find((turn) => turn.status === "completed");
|
|
707
|
+
const toolCounts = lastTurn ? summarizeTurnTools(lastTurn) : undefined;
|
|
708
|
+
const status = remoteThreadStatus(thread.status?.type);
|
|
709
|
+
// A persisted inProgress turn can outlive the CLI/Desktop app-server process
|
|
710
|
+
// that owned it. Only the owning app-server's active thread status (or the
|
|
711
|
+
// rollout activity detector applied below) is evidence that it is still live.
|
|
712
|
+
const lastTurnStatus = lastTurn?.status === "inProgress" && status !== "active"
|
|
713
|
+
? undefined
|
|
714
|
+
: lastTurn?.status;
|
|
715
|
+
const lastText = [...(lastTurn?.items ?? [])]
|
|
716
|
+
.reverse()
|
|
717
|
+
.find((item) => typeof item.text === "string" && item.text.trim())?.text?.trim();
|
|
718
|
+
return {
|
|
719
|
+
id: thread.id,
|
|
720
|
+
title: normalizeTaskTitle(thread.name) ?? normalizeTaskTitle(thread.preview),
|
|
721
|
+
preview: normalizeTaskTitle(thread.preview),
|
|
722
|
+
cwd: thread.cwd ?? "",
|
|
723
|
+
source: codexSourceLabel(thread.source),
|
|
724
|
+
status,
|
|
725
|
+
createdAt: thread.createdAt,
|
|
726
|
+
updatedAt: thread.updatedAt,
|
|
727
|
+
recencyAt: thread.recencyAt ?? undefined,
|
|
728
|
+
lastTurnId: lastTurn?.id,
|
|
729
|
+
lastCompletedTurnId: lastCompletedTurn?.id,
|
|
730
|
+
lastTurnStatus,
|
|
731
|
+
lastActivity: lastText,
|
|
732
|
+
finalResponse: lastTurn && lastTurn.status !== "inProgress" ? extractFinalResponse(lastTurn) || undefined : undefined,
|
|
733
|
+
lastError: lastTurn?.error?.message,
|
|
734
|
+
lastTurnToolCount: toolCounts?.total,
|
|
735
|
+
lastTurnCompletedToolCount: toolCounts?.completed,
|
|
736
|
+
lastTurnFailedToolCount: toolCounts?.failed,
|
|
737
|
+
lastTurnRunningToolCount: toolCounts?.running,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function summarizeTurnTools(turn) {
|
|
741
|
+
const tools = (turn.items ?? []).filter((item) => isToolItemType(item.type));
|
|
742
|
+
let completed = 0;
|
|
743
|
+
let failed = 0;
|
|
744
|
+
let running = 0;
|
|
745
|
+
for (const tool of tools) {
|
|
746
|
+
if (tool.status === "failed" || tool.status === "declined")
|
|
747
|
+
failed += 1;
|
|
748
|
+
else if (tool.status === "inProgress" || tool.status === "running")
|
|
749
|
+
running += 1;
|
|
750
|
+
else
|
|
751
|
+
completed += 1;
|
|
752
|
+
}
|
|
753
|
+
return { total: tools.length, completed, failed, running };
|
|
754
|
+
}
|
|
755
|
+
function isToolItemType(type) {
|
|
756
|
+
return type === "commandExecution"
|
|
757
|
+
|| type === "fileChange"
|
|
758
|
+
|| type === "mcpToolCall"
|
|
759
|
+
|| type === "dynamicToolCall"
|
|
760
|
+
|| type === "webSearch"
|
|
761
|
+
|| type === "imageView";
|
|
762
|
+
}
|
|
763
|
+
function mergeRemoteSessionSummary(listed, inspected) {
|
|
764
|
+
return {
|
|
765
|
+
...listed,
|
|
766
|
+
...inspected,
|
|
767
|
+
title: inspected.title ?? listed.title,
|
|
768
|
+
preview: inspected.preview ?? listed.preview,
|
|
769
|
+
cwd: inspected.cwd || listed.cwd,
|
|
770
|
+
source: inspected.source === "unknown" ? listed.source : inspected.source,
|
|
771
|
+
createdAt: inspected.createdAt ?? listed.createdAt,
|
|
772
|
+
updatedAt: inspected.updatedAt ?? listed.updatedAt,
|
|
773
|
+
recencyAt: inspected.recencyAt ?? listed.recencyAt,
|
|
774
|
+
lastActivity: inspected.lastActivity ?? listed.lastActivity,
|
|
775
|
+
finalResponse: inspected.finalResponse ?? listed.finalResponse,
|
|
776
|
+
lastError: inspected.lastError ?? listed.lastError,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function markLocallyDetectedActive(summary, activeThreads) {
|
|
780
|
+
if (!activeThreads.has(summary.id))
|
|
781
|
+
return summary;
|
|
782
|
+
const detectedTurnId = activeThreads.get(summary.id);
|
|
783
|
+
if ((summary.status === "active" || summary.lastTurnStatus === "inProgress")
|
|
784
|
+
&& (!detectedTurnId || summary.lastTurnId === detectedTurnId))
|
|
785
|
+
return summary;
|
|
786
|
+
return {
|
|
787
|
+
...summary,
|
|
788
|
+
status: "active",
|
|
789
|
+
lastTurnId: detectedTurnId ?? summary.lastTurnId,
|
|
790
|
+
lastTurnStatus: "inProgress",
|
|
791
|
+
lastActivity: undefined,
|
|
792
|
+
finalResponse: undefined,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function codexUserInput(prompt) {
|
|
796
|
+
const normalized = typeof prompt === "string" ? { text: prompt, localImagePaths: [] } : {
|
|
797
|
+
text: prompt.text,
|
|
798
|
+
localImagePaths: prompt.localImagePaths ?? [],
|
|
799
|
+
};
|
|
800
|
+
return [
|
|
801
|
+
...(normalized.text.trim() ? [{ type: "text", text: normalized.text, text_elements: [] }] : []),
|
|
802
|
+
...normalized.localImagePaths.map((imagePath) => ({ type: "localImage", path: imagePath })),
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
function remoteThreadStatus(value) {
|
|
806
|
+
if (value === "active")
|
|
807
|
+
return "active";
|
|
808
|
+
if (value === "idle")
|
|
809
|
+
return "idle";
|
|
810
|
+
if (value === "systemError")
|
|
811
|
+
return "error";
|
|
812
|
+
return "not_loaded";
|
|
813
|
+
}
|
|
814
|
+
function codexSourceLabel(source) {
|
|
815
|
+
if (typeof source === "string")
|
|
816
|
+
return source;
|
|
817
|
+
if (isRecord(source)) {
|
|
818
|
+
if (typeof source.custom === "string")
|
|
819
|
+
return source.custom;
|
|
820
|
+
if (source.subAgent !== undefined)
|
|
821
|
+
return "subAgent";
|
|
822
|
+
}
|
|
823
|
+
return "unknown";
|
|
824
|
+
}
|
|
825
|
+
//# sourceMappingURL=CodexRuntime.js.map
|