@konglx/rotom 2.21.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/README.md +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Executor
|
|
3
|
+
*
|
|
4
|
+
* Spawns `codex app-server --listen stdio://` and drives the Codex JSON-RPC 2.0
|
|
5
|
+
* protocol over stdin/stdout. Mirrors Multica's Go reference implementation in
|
|
6
|
+
* server/pkg/agent/codex.go.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* 1. initialize handshake
|
|
10
|
+
* 2. `initialized` notification
|
|
11
|
+
* 3. thread/start (or thread/resume when sessionId is given) → threadId
|
|
12
|
+
* 4. turn/start with threadId + prompt
|
|
13
|
+
* 5. wait for turn/completed (raw v2) or task_complete (legacy codex/event)
|
|
14
|
+
*
|
|
15
|
+
* Two notification dialects are supported:
|
|
16
|
+
* • Legacy: { method: "codex/event", params: { msg: { type, ... } } }
|
|
17
|
+
* • Raw v2: { method: "turn/started" | "turn/completed" | "item/<phase>", ... }
|
|
18
|
+
*
|
|
19
|
+
* Server-initiated approval requests:
|
|
20
|
+
* • exec / file change → routed to options.onApprovalRequest when provided
|
|
21
|
+
* (so a human can Accept/Deny via the dashboard); otherwise auto-accepted
|
|
22
|
+
* to keep daemon-style runs unblocked.
|
|
23
|
+
* • MCP elicitation → always auto-accepted (internal protocol chatter).
|
|
24
|
+
*
|
|
25
|
+
* Whether `onApprovalRequest` is supplied is decided by the worker based on
|
|
26
|
+
* `issue.approval_policy`: `r_allow`(默认)→ 传 callback;`rw_allow` → 不传,
|
|
27
|
+
* 走 auto-accept 路径。
|
|
28
|
+
*/
|
|
29
|
+
import { runProcess } from "../process-runner.js";
|
|
30
|
+
import { createJsonRpcTransport } from "../jsonrpc-transport.js";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import { buildPlanModeInstruction } from "../../shared/slash-commands.js";
|
|
35
|
+
import { emitStatus } from "../reasoning-status.js";
|
|
36
|
+
// ── Executor ────────────────────────────────────────────────────────────
|
|
37
|
+
export class CodexExecutor {
|
|
38
|
+
async execute(prompt, workingDir, onOutput, options) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const resumeSessionId = options?.sessionId || "";
|
|
41
|
+
// prompt 已经由 worker 用 composePrompt() 拼好,executor 不再二次包装。
|
|
42
|
+
const args = ["app-server", "--listen", "stdio://"];
|
|
43
|
+
const spawnEnv = { ...process.env, ...options?.env };
|
|
44
|
+
const { proc, done: procDone } = runProcess({
|
|
45
|
+
bin: "codex",
|
|
46
|
+
args,
|
|
47
|
+
cwd: workingDir,
|
|
48
|
+
env: spawnEnv,
|
|
49
|
+
label: "codex",
|
|
50
|
+
signal: options?.signal,
|
|
51
|
+
});
|
|
52
|
+
// ── Per-run state ──
|
|
53
|
+
let fullOutput = "";
|
|
54
|
+
let threadId = "";
|
|
55
|
+
let settled = false;
|
|
56
|
+
let failed = false;
|
|
57
|
+
let turnError = "";
|
|
58
|
+
let sessionPoisoned = false;
|
|
59
|
+
let turnDoneResolve = null;
|
|
60
|
+
let notificationProtocol = "unknown";
|
|
61
|
+
let turnStarted = false;
|
|
62
|
+
// 终端状态去重:codex v2 协议下可能从多个路径到达终态(item/completed
|
|
63
|
+
// agentMessage final_answer / turn/completed / finish()),dashboard
|
|
64
|
+
// 端 hoistStatus 已经只保留最后一个 tag,但重复 emit 既浪费流量也
|
|
65
|
+
// 干扰调试日志,这里用同一个 flag 集中拦截。
|
|
66
|
+
let terminalEmitted = false;
|
|
67
|
+
const completedTurnIds = new Set();
|
|
68
|
+
let capturedUsage;
|
|
69
|
+
let capturedModel;
|
|
70
|
+
const turnDone = new Promise((res) => { turnDoneResolve = res; });
|
|
71
|
+
function signalTurnDone(aborted) {
|
|
72
|
+
if (turnDoneResolve) {
|
|
73
|
+
const r = turnDoneResolve;
|
|
74
|
+
turnDoneResolve = null;
|
|
75
|
+
r(aborted);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function setTurnError(msg) {
|
|
79
|
+
if (msg && !turnError)
|
|
80
|
+
turnError = msg;
|
|
81
|
+
// Detect the upstream OpenAI invariant violation that pins a chat
|
|
82
|
+
// history forever once an assistant `tool_calls` message exists
|
|
83
|
+
// without matching tool responses. Once this is set, callers should
|
|
84
|
+
// drop the cached sessionId so the next run starts fresh — otherwise
|
|
85
|
+
// every resume keeps replaying the poisoned history.
|
|
86
|
+
if (msg && /tool[_ ]?calls?|tool[_ ]message|tool_call_id/i.test(msg)) {
|
|
87
|
+
sessionPoisoned = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── JSON-RPC transport (line framing + pending map + onRequest/onNotification) ──
|
|
91
|
+
// The transport owns the readline loop and routes each frame to the
|
|
92
|
+
// matching callback. We just plug in our domain handlers below.
|
|
93
|
+
const transport = createJsonRpcTransport({
|
|
94
|
+
stdin: proc.stdin,
|
|
95
|
+
stdout: proc.stdout,
|
|
96
|
+
label: "codex",
|
|
97
|
+
onRequest: (method, params, id) => handleServerRequest(method, params, id),
|
|
98
|
+
onNotification: (method, params) => handleNotification(method, params),
|
|
99
|
+
});
|
|
100
|
+
function request(method, params) {
|
|
101
|
+
return transport.request(method, params);
|
|
102
|
+
}
|
|
103
|
+
function notify(method, params) {
|
|
104
|
+
transport.notify(method, params);
|
|
105
|
+
}
|
|
106
|
+
function respond(id, result) {
|
|
107
|
+
transport.respond(id, result);
|
|
108
|
+
}
|
|
109
|
+
function respondError(id, code, message) {
|
|
110
|
+
transport.respondError(id, code, message);
|
|
111
|
+
}
|
|
112
|
+
// ── Server → client requests (auto-approve) ──
|
|
113
|
+
function handleServerRequest(method, rawParams, id) {
|
|
114
|
+
const params = (rawParams ?? {});
|
|
115
|
+
switch (method) {
|
|
116
|
+
case "item/commandExecution/requestApproval":
|
|
117
|
+
case "execCommandApproval": {
|
|
118
|
+
const input = extractExecApprovalInput(params);
|
|
119
|
+
routeApproval(id, input);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
case "item/fileChange/requestApproval":
|
|
123
|
+
case "applyPatchApproval": {
|
|
124
|
+
const input = extractFileApprovalInput(params);
|
|
125
|
+
routeApproval(id, input);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case "mcpServer/elicitation/request":
|
|
129
|
+
// MCP elicitations stay auto-accepted — they're internal protocol
|
|
130
|
+
// chatter that the human user does not need to vet.
|
|
131
|
+
respond(id, { action: "accept", content: null, _meta: null });
|
|
132
|
+
return;
|
|
133
|
+
default:
|
|
134
|
+
console.warn(`[codex] unhandled server request: ${method}`);
|
|
135
|
+
respondError(id, -32601, `unhandled server request: ${method}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Bridge to the worker's approval pipeline. If no callback is wired,
|
|
139
|
+
// fall back to the legacy auto-accept so daemon contexts keep working.
|
|
140
|
+
function routeApproval(id, input) {
|
|
141
|
+
if (!options?.onApprovalRequest) {
|
|
142
|
+
respond(id, { decision: "accept" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Fire-and-forget — we intentionally don't await this. codex stays
|
|
146
|
+
// parked on the JSON-RPC request until we call respond() below.
|
|
147
|
+
void (async () => {
|
|
148
|
+
let result = { decision: "deny" };
|
|
149
|
+
try {
|
|
150
|
+
result = await options.onApprovalRequest(input);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.warn(`[codex] approval callback threw, defaulting to deny: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
// Send the optional user-supplied feedback as `reason` on denials so
|
|
156
|
+
// codex can surface it back to the model. Unknown fields are ignored
|
|
157
|
+
// by JSON-RPC peers, so this is safe even on codex builds that don't
|
|
158
|
+
// read `reason`.
|
|
159
|
+
if (result.decision === "deny" && result.feedback?.trim()) {
|
|
160
|
+
respond(id, { decision: "deny", reason: result.feedback.trim() });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
respond(id, { decision: result.decision });
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
}
|
|
167
|
+
// ── Notification handling ──
|
|
168
|
+
function handleNotification(method, rawParams) {
|
|
169
|
+
const params = (rawParams ?? {});
|
|
170
|
+
if (process.env.ROTOM_CODEX_DEBUG) {
|
|
171
|
+
console.log(`[codex DEBUG] notification method=${method} params=${JSON.stringify(params).slice(0, 600)}`);
|
|
172
|
+
}
|
|
173
|
+
// Legacy: codex/event
|
|
174
|
+
if (method === "codex/event" || method.startsWith("codex/event/")) {
|
|
175
|
+
notificationProtocol = "legacy";
|
|
176
|
+
// 新版 codex 可能把 event_msg payload 直接放在 params 顶层(无 msg 包装),
|
|
177
|
+
// 旧版放在 params.msg。两种都接受,避免 token_count / session_meta 这类
|
|
178
|
+
// 事件被静默丢弃。
|
|
179
|
+
const msg = (params.msg ?? params);
|
|
180
|
+
if (msg && typeof msg === "object" && "type" in msg)
|
|
181
|
+
handleLegacyEvent(msg);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (notificationProtocol === "legacy")
|
|
185
|
+
return;
|
|
186
|
+
if (notificationProtocol === "unknown" &&
|
|
187
|
+
(method === "turn/started" ||
|
|
188
|
+
method === "turn/completed" ||
|
|
189
|
+
method === "thread/started" ||
|
|
190
|
+
method.startsWith("item/"))) {
|
|
191
|
+
notificationProtocol = "raw";
|
|
192
|
+
}
|
|
193
|
+
if (notificationProtocol === "raw")
|
|
194
|
+
handleRawNotification(method, params);
|
|
195
|
+
}
|
|
196
|
+
function handleLegacyEvent(msg) {
|
|
197
|
+
const type = msg.type;
|
|
198
|
+
switch (type) {
|
|
199
|
+
case "task_started":
|
|
200
|
+
turnStarted = true;
|
|
201
|
+
emitStatus(onOutput, "Working");
|
|
202
|
+
return;
|
|
203
|
+
case "agent_message": {
|
|
204
|
+
const text = msg.message;
|
|
205
|
+
if (text) {
|
|
206
|
+
fullOutput += text;
|
|
207
|
+
onOutput(text);
|
|
208
|
+
// 不要在每个 chunk 都 emit "Working" — turn/started 已经发过了,
|
|
209
|
+
// 这里再发反而会把 "Running" 状态打回 "Working",导致工具调用期间
|
|
210
|
+
// pill 在两个状态之间闪烁。
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case "exec_command_begin": {
|
|
215
|
+
const command = msg.command;
|
|
216
|
+
onOutput(`[tool:exec]${prettyCommand(command ?? "")}[/tool:exec]\n`);
|
|
217
|
+
emitStatus(onOutput, "Running");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
case "exec_command_end": {
|
|
221
|
+
const output = msg.output ?? "";
|
|
222
|
+
const truncated = output.length > 500 ? `${output.slice(0, 500)}...` : output;
|
|
223
|
+
if (truncated)
|
|
224
|
+
onOutput(`[tool-result:exec]${truncated}[/tool-result:exec]\n`);
|
|
225
|
+
// codex 不在工具完成时发 "Done" — tool 完成后模型进入 thinking,
|
|
226
|
+
// 下一个 event (新 tool call / final_answer) 会自己覆盖 status。
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
case "patch_apply_begin":
|
|
230
|
+
onOutput(`[tool:patch]apply[/tool:patch]\n`);
|
|
231
|
+
emitStatus(onOutput, "Patching");
|
|
232
|
+
return;
|
|
233
|
+
case "patch_apply_end":
|
|
234
|
+
// 同上:patch 完成不发 "Patched",让 status 保持上一个状态,等下一个
|
|
235
|
+
// event 覆盖。
|
|
236
|
+
return;
|
|
237
|
+
case "task_complete":
|
|
238
|
+
signalTurnDone(false);
|
|
239
|
+
return;
|
|
240
|
+
case "turn_aborted":
|
|
241
|
+
signalTurnDone(true);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function handleRawNotification(method, params) {
|
|
246
|
+
// Codex multiplexes child threads on the same stdio pipe; ignore
|
|
247
|
+
// notifications that don't belong to the thread we started.
|
|
248
|
+
const eventThreadId = params.threadId;
|
|
249
|
+
if (eventThreadId && threadId && eventThreadId !== threadId)
|
|
250
|
+
return;
|
|
251
|
+
switch (method) {
|
|
252
|
+
case "thread/started": {
|
|
253
|
+
// thread/started 的 params.thread.modelProvider 是后端 provider 名
|
|
254
|
+
// (e.g. "deepseek" / "openai"),用作 model 展示。codex 实际模型名
|
|
255
|
+
// (e.g. "deepseek-v4-flash")只在某些 warning 文本里出现,不稳。
|
|
256
|
+
const threadObj = (params.thread ?? {});
|
|
257
|
+
const provider = threadObj.modelProvider;
|
|
258
|
+
if (provider && !capturedModel)
|
|
259
|
+
capturedModel = provider;
|
|
260
|
+
// 落到下方 turn/started 之外的处理:thread/started 不算 turn 开始,
|
|
261
|
+
// 不发 Working 状态。直接 return,不要触发 turn/started 的逻辑。
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "turn/started":
|
|
265
|
+
turnStarted = true;
|
|
266
|
+
emitStatus(onOutput, "Working");
|
|
267
|
+
return;
|
|
268
|
+
case "thread/tokenUsage/updated": {
|
|
269
|
+
// codex v2 真正的 usage 通知。params.tokenUsage.total 是该 thread
|
|
270
|
+
// 累计用量(input + 历史 turns),last 是本轮。我们存 total —— 一个
|
|
271
|
+
// issue 可能多次 turn,最终值就是整个 issue 执行的总量。
|
|
272
|
+
// 字段是 camelCase: inputTokens / outputTokens / cachedInputTokens /
|
|
273
|
+
// reasoningOutputTokens(注意不是 cache_read_input_tokens)。
|
|
274
|
+
const tokenUsage = (params.tokenUsage ?? {});
|
|
275
|
+
const total = (tokenUsage.total ?? {});
|
|
276
|
+
capturedUsage = {
|
|
277
|
+
inputTokens: typeof total.inputTokens === "number" ? total.inputTokens : undefined,
|
|
278
|
+
outputTokens: typeof total.outputTokens === "number" ? total.outputTokens : undefined,
|
|
279
|
+
cacheReadTokens: typeof total.cachedInputTokens === "number" ? total.cachedInputTokens : undefined,
|
|
280
|
+
cacheCreationTokens: undefined,
|
|
281
|
+
totalCostUsd: undefined,
|
|
282
|
+
};
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
case "turn/completed": {
|
|
286
|
+
const turn = (params.turn ?? {});
|
|
287
|
+
const turnId = turn.id ?? "";
|
|
288
|
+
const status = turn.status ?? "";
|
|
289
|
+
const aborted = status === "cancelled" ||
|
|
290
|
+
status === "canceled" ||
|
|
291
|
+
status === "aborted" ||
|
|
292
|
+
status === "interrupted";
|
|
293
|
+
if (process.env.ROTOM_CODEX_DEBUG) {
|
|
294
|
+
console.log(`[codex DEBUG] turn/completed params=${JSON.stringify(params).slice(0, 800)}`);
|
|
295
|
+
}
|
|
296
|
+
if (status === "failed") {
|
|
297
|
+
const err = turn.error ?? {};
|
|
298
|
+
setTurnError(err.message || "codex turn failed");
|
|
299
|
+
failed = true;
|
|
300
|
+
if (!terminalEmitted) {
|
|
301
|
+
emitStatus(onOutput, "Failed");
|
|
302
|
+
terminalEmitted = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (turnId) {
|
|
306
|
+
if (completedTurnIds.has(turnId))
|
|
307
|
+
return;
|
|
308
|
+
completedTurnIds.add(turnId);
|
|
309
|
+
}
|
|
310
|
+
// 兜底:agentMessage final_answer 没拿到 phase / 没下发时,
|
|
311
|
+
// 仍然需要一个终态 pill,否则 dashboard 一直停在 "Working"。
|
|
312
|
+
// aborted 留给上一次的 non-terminal 状态(pill 视觉上停在"被打断
|
|
313
|
+
// 那一刻"),不强行覆盖。
|
|
314
|
+
if (!terminalEmitted && !aborted && status !== "failed") {
|
|
315
|
+
emitStatus(onOutput, "Answered");
|
|
316
|
+
terminalEmitted = true;
|
|
317
|
+
}
|
|
318
|
+
signalTurnDone(aborted);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
case "error": {
|
|
322
|
+
const willRetry = params.willRetry === true;
|
|
323
|
+
const errMsg = params.error?.message ||
|
|
324
|
+
params.message ||
|
|
325
|
+
"";
|
|
326
|
+
if (errMsg) {
|
|
327
|
+
console.warn(`[codex] error notification: ${errMsg} (willRetry=${willRetry})`);
|
|
328
|
+
if (!willRetry) {
|
|
329
|
+
setTurnError(errMsg);
|
|
330
|
+
failed = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case "thread/status/changed": {
|
|
336
|
+
const statusType = params.status?.type ?? "";
|
|
337
|
+
if (statusType === "idle" && turnStarted)
|
|
338
|
+
signalTurnDone(false);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
default:
|
|
342
|
+
if (method.startsWith("item/"))
|
|
343
|
+
handleItemNotification(method, params);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function handleItemNotification(method, params) {
|
|
347
|
+
const item = params.item;
|
|
348
|
+
if (!item)
|
|
349
|
+
return;
|
|
350
|
+
const itemType = item.type;
|
|
351
|
+
const itemId = item.id;
|
|
352
|
+
if (process.env.ROTOM_CODEX_DEBUG) {
|
|
353
|
+
console.log(`[codex DEBUG] handleItemNotification method=${method} itemType=${itemType} itemId=${itemId} phase=${item.phase ?? "(none)"} command=${String(item.command).slice(0, 60)}`);
|
|
354
|
+
}
|
|
355
|
+
// 用字段存在性判断 item 类型,而不是严格匹配 `type` 字符串 — 不同 codex
|
|
356
|
+
// 版本对 enum variant 的序列化格式不一样(camelCase / snake_case / 别名),
|
|
357
|
+
// 而且 codex 还在持续迭代(2026.5 的 v2 协议又引入了新 ThreadItem 变体),
|
|
358
|
+
// 紧耦合 type 字符串会让这里脆弱。commandExecution 一定有 `command` 字段
|
|
359
|
+
// (string 或 string[]),fileChange 一定有 `changes` / `patch` 字段。
|
|
360
|
+
const looksLikeCommandExec = typeof item.command !== "undefined";
|
|
361
|
+
const looksLikeFileChange = item.changes !== undefined ||
|
|
362
|
+
item.patch !== undefined;
|
|
363
|
+
if (method === "item/started" && looksLikeCommandExec) {
|
|
364
|
+
const rawCmd = item.command;
|
|
365
|
+
const command = Array.isArray(rawCmd)
|
|
366
|
+
? rawCmd.map((p) => String(p)).join(" ")
|
|
367
|
+
: (rawCmd ?? "");
|
|
368
|
+
onOutput(`[tool:exec]${prettyCommand(command)}[/tool:exec]\n`);
|
|
369
|
+
emitStatus(onOutput, "Running");
|
|
370
|
+
if (process.env.ROTOM_CODEX_DEBUG) {
|
|
371
|
+
console.log(`[codex DEBUG] emitted [tool:exec] + Running status for command=${command.slice(0, 60)}`);
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (method === "item/completed" && looksLikeCommandExec) {
|
|
376
|
+
const output = item.aggregatedOutput ??
|
|
377
|
+
item.output ??
|
|
378
|
+
"";
|
|
379
|
+
const truncated = output.length > 500 ? `${output.slice(0, 500)}...` : output;
|
|
380
|
+
if (truncated)
|
|
381
|
+
onOutput(`[tool-result:exec]${truncated}[/tool-result:exec]\n`);
|
|
382
|
+
// codex 不在 tool 完成时发 "Done" — 让 status 保持上一个状态,
|
|
383
|
+
// 等下一个 event (新 tool / final_answer) 自己覆盖。
|
|
384
|
+
// exitCode != 0 走 turn/completed 的 failed 分支,这里不再发 Failed,
|
|
385
|
+
// 避免一个失败的 tool 就把整个 turn 标红。
|
|
386
|
+
void item.exitCode;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (method === "item/started" && looksLikeFileChange) {
|
|
390
|
+
onOutput(`[tool:patch]apply[/tool:patch]\n`);
|
|
391
|
+
emitStatus(onOutput, "Patching");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (method === "item/completed" && looksLikeFileChange) {
|
|
395
|
+
// 同上:patch 完成后不发 "Patched"。
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// codex v2 协议下 agent message 是流式推送的,文本通过
|
|
399
|
+
// `item/agentMessage/delta` 增量进来(delta 字段),完整消息在
|
|
400
|
+
// `item/completed` 时带 phase 标识(Commentary / FinalAnswer)。
|
|
401
|
+
// 不处理 delta 的话,dashboard 端 streaming 期间看不到 agent 文字,
|
|
402
|
+
// pill 也永远卡在 "Working"(因为 turn/started 后没有新 status emit)。
|
|
403
|
+
if (method === "item/agentMessage/delta") {
|
|
404
|
+
const delta = params.delta ?? "";
|
|
405
|
+
if (delta) {
|
|
406
|
+
fullOutput += delta;
|
|
407
|
+
onOutput(delta);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (method === "item/completed" && itemType === "agentMessage") {
|
|
412
|
+
const text = item.text ?? "";
|
|
413
|
+
if (text) {
|
|
414
|
+
// 兜底:有些 codex 版本把完整文本塞在 item.completed 的 text 字段,
|
|
415
|
+
// 而不用 delta 流推。这里只在 fullOutput 还没有这段文本时 push,
|
|
416
|
+
// 避免和 delta 重复输出。
|
|
417
|
+
if (!fullOutput.endsWith(text)) {
|
|
418
|
+
fullOutput += text;
|
|
419
|
+
onOutput(text);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const phase = item.phase;
|
|
423
|
+
// codex v2 协议 phase 取值是 PascalCase("FinalAnswer"/"Commentary"),
|
|
424
|
+
// 见上方注释。大小写都接受,避免某个 minor 版本切回 snake_case 时
|
|
425
|
+
// 又把 "Answered" 丢掉。
|
|
426
|
+
if (phase && phase.toLowerCase() === "final_answer") {
|
|
427
|
+
// "Answered" 是 terminal state,pill 会停在这里。
|
|
428
|
+
if (!terminalEmitted) {
|
|
429
|
+
emitStatus(onOutput, "Answered");
|
|
430
|
+
terminalEmitted = true;
|
|
431
|
+
}
|
|
432
|
+
if (turnStarted)
|
|
433
|
+
signalTurnDone(false);
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// itemId is reserved for future per-tool tracking; reference to silence lint.
|
|
438
|
+
void itemId;
|
|
439
|
+
}
|
|
440
|
+
// ── Line router ──
|
|
441
|
+
// (The transport above already routes server requests / notifications;
|
|
442
|
+
// responses are matched against the transport's pending map and resolve
|
|
443
|
+
// the corresponding `transport.request(...)` promises automatically.)
|
|
444
|
+
proc.stderr.on("data", (data) => {
|
|
445
|
+
const text = data.toString().trim();
|
|
446
|
+
if (text)
|
|
447
|
+
console.error(`[codex] stderr: ${text}`);
|
|
448
|
+
});
|
|
449
|
+
function finish(exitCode) {
|
|
450
|
+
if (settled)
|
|
451
|
+
return;
|
|
452
|
+
settled = true;
|
|
453
|
+
// Reject any leftover pending requests so dangling promises don't keep the event loop alive.
|
|
454
|
+
transport.rejectPending(new Error("codex process exited"));
|
|
455
|
+
// 最后一道防线:某些 codex 边角场景下进程干净退出但 turn/completed
|
|
456
|
+
// 路径没走到(或没匹配上),pill 会一直停在 "Working"。这里在 exit
|
|
457
|
+
// code 0 且没下发过任何终态时,补发 "Answered"。fail / 非零退出
|
|
458
|
+
// 路径交给 worker 通过 failed flag 标红,不在这里掺合。
|
|
459
|
+
if (!terminalEmitted && exitCode === 0) {
|
|
460
|
+
emitStatus(onOutput, "Answered");
|
|
461
|
+
terminalEmitted = true;
|
|
462
|
+
}
|
|
463
|
+
const reportedSessionId = resolveSessionId(resumeSessionId, threadId, failed || exitCode !== 0);
|
|
464
|
+
const finalCode = failed && exitCode === 0 ? 1 : exitCode;
|
|
465
|
+
console.log(`[codex] Exited code=${exitCode}, output=${fullOutput.length} chars, session=${reportedSessionId}, poisoned=${sessionPoisoned}`);
|
|
466
|
+
resolve({
|
|
467
|
+
exitCode: finalCode,
|
|
468
|
+
fullOutput,
|
|
469
|
+
sessionId: sessionPoisoned ? undefined : (reportedSessionId || undefined),
|
|
470
|
+
invalidateSession: sessionPoisoned || undefined,
|
|
471
|
+
usage: capturedUsage,
|
|
472
|
+
model: capturedModel,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
proc.on("close", (code) => finish(code ?? 1));
|
|
476
|
+
proc.on("error", (err) => {
|
|
477
|
+
console.error(`[codex] Spawn error: ${err.message}`);
|
|
478
|
+
finish(1);
|
|
479
|
+
});
|
|
480
|
+
// ── Drive the protocol ──
|
|
481
|
+
void (async () => {
|
|
482
|
+
try {
|
|
483
|
+
await request("initialize", {
|
|
484
|
+
clientInfo: {
|
|
485
|
+
name: "open-a2a-gateway",
|
|
486
|
+
title: "Open A2A WORKSPACE",
|
|
487
|
+
version: "0.1.0",
|
|
488
|
+
},
|
|
489
|
+
capabilities: { experimentalApi: true },
|
|
490
|
+
});
|
|
491
|
+
notify("initialized");
|
|
492
|
+
threadId = await startOrResumeThread(request, resumeSessionId, workingDir, options?.slashCommand);
|
|
493
|
+
await request("turn/start", {
|
|
494
|
+
threadId,
|
|
495
|
+
input: [{ type: "text", text: prompt }],
|
|
496
|
+
});
|
|
497
|
+
const aborted = await turnDone;
|
|
498
|
+
if (aborted) {
|
|
499
|
+
failed = true;
|
|
500
|
+
setTurnError("turn was aborted");
|
|
501
|
+
}
|
|
502
|
+
else if (turnError) {
|
|
503
|
+
failed = true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
failed = true;
|
|
508
|
+
const msg = err.message;
|
|
509
|
+
console.error(`[codex] lifecycle error: ${msg}`);
|
|
510
|
+
setTurnError(msg);
|
|
511
|
+
onOutput(`[error] ${msg}\n`);
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
try {
|
|
515
|
+
proc.stdin?.end();
|
|
516
|
+
}
|
|
517
|
+
catch { /* noop */ }
|
|
518
|
+
}
|
|
519
|
+
})();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Read the tail of codex's session transcript from its local JSONL store.
|
|
524
|
+
* Codex writes one file per session under
|
|
525
|
+
* `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<ISO>-<sessionId>.jsonl`
|
|
526
|
+
* with the sessionId as the trailing UUID before the `.jsonl` extension. We
|
|
527
|
+
* walk the tree (find by suffix is cheaper than maintaining a date index)
|
|
528
|
+
* and return the last N lines verbatim — the dashboard renders them as a
|
|
529
|
+
* `<pre>` block.
|
|
530
|
+
*
|
|
531
|
+
* Tolerant of missing files (codex may prune its sessions directory) —
|
|
532
|
+
* returns empty content + an explanatory `error` so the dashboard can
|
|
533
|
+
* distinguish "file gone" from "session started but no output yet".
|
|
534
|
+
*/
|
|
535
|
+
async readSessionContent(args) {
|
|
536
|
+
const file = findCodexRolloutFile(args.sessionId);
|
|
537
|
+
if (!file) {
|
|
538
|
+
return {
|
|
539
|
+
format: "jsonl",
|
|
540
|
+
content: "",
|
|
541
|
+
error: "codex session 文件不存在(可能已被 codex CLI 清理)",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
545
|
+
const lines = text.split("\n");
|
|
546
|
+
const tail = args.tailLines ?? 200;
|
|
547
|
+
const sliced = lines.length > tail ? lines.slice(-tail).join("\n") : text;
|
|
548
|
+
return { format: "jsonl", content: sliced };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// ── Codex session-file lookup ───────────────────────────────────────────
|
|
552
|
+
/**
|
|
553
|
+
* Walk ~/.codex/sessions/<YYYY>/<MM>/<DD>/ and find the rollout JSONL whose
|
|
554
|
+
* filename ends with `-<sessionId>.jsonl`. Returns the absolute path or null
|
|
555
|
+
* if no match. The 3-level date directory is what codex's `~/.codex/sessions`
|
|
556
|
+
* layout uses in 2026.5+; on older builds the same files live flat under
|
|
557
|
+
* `~/.codex/sessions/` so we walk two levels deep just in case.
|
|
558
|
+
*/
|
|
559
|
+
function findCodexRolloutFile(sessionId) {
|
|
560
|
+
const root = path.join(os.homedir(), ".codex", "sessions");
|
|
561
|
+
if (!fs.existsSync(root))
|
|
562
|
+
return null;
|
|
563
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
564
|
+
const stack = [root];
|
|
565
|
+
while (stack.length > 0) {
|
|
566
|
+
const dir = stack.pop();
|
|
567
|
+
let entries;
|
|
568
|
+
try {
|
|
569
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
for (const e of entries) {
|
|
575
|
+
const full = path.join(dir, e.name);
|
|
576
|
+
if (e.isDirectory()) {
|
|
577
|
+
stack.push(full);
|
|
578
|
+
}
|
|
579
|
+
else if (e.isFile() && e.name.endsWith(suffix)) {
|
|
580
|
+
return full;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
587
|
+
async function startOrResumeThread(request, resumeSessionId, cwd, slashCommand) {
|
|
588
|
+
// /plan → codex 没有原生 plan 模式,靠 developerInstructions 注入开发者级
|
|
589
|
+
// 系统提示,引导其"先方案后落盘"。注册表见 src/shared/slash-commands.ts。
|
|
590
|
+
const developerInstructions = slashCommand === "/plan" ? buildPlanModeInstruction() : null;
|
|
591
|
+
if (resumeSessionId) {
|
|
592
|
+
try {
|
|
593
|
+
const res = (await request("thread/resume", {
|
|
594
|
+
threadId: resumeSessionId,
|
|
595
|
+
cwd,
|
|
596
|
+
model: null,
|
|
597
|
+
developerInstructions,
|
|
598
|
+
// 同 thread/start:codex 默认 read-only 沙箱挡 127.0.0.1 回环,
|
|
599
|
+
// rotom CLI 子命令全部 fetch failed。chat 路径默认走 resume
|
|
600
|
+
// (~/.rotom/sessions.json 缓存),不补这行修复永远不生效。
|
|
601
|
+
// 详见 docs/codex-sandbox-network-blocked.md。
|
|
602
|
+
sandbox: "danger-full-access",
|
|
603
|
+
}));
|
|
604
|
+
const id = extractThreadId(res);
|
|
605
|
+
if (id)
|
|
606
|
+
return id;
|
|
607
|
+
console.warn(`[codex] thread/resume returned no thread id; falling back to thread/start (prior=${resumeSessionId})`);
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
console.warn(`[codex] thread/resume failed; falling back to thread/start: ${err.message}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const res = (await request("thread/start", {
|
|
614
|
+
model: null,
|
|
615
|
+
modelProvider: null,
|
|
616
|
+
profile: null,
|
|
617
|
+
cwd,
|
|
618
|
+
approvalPolicy: null,
|
|
619
|
+
// codex CLI 默认 read-only 沙箱挡掉所有出站网络,包括 127.0.0.1 回环 ——
|
|
620
|
+
// rotom CLI 任何子命令都打不到 master,统一报 "fetch failed"。worker 是
|
|
621
|
+
// 受信进程、cwd 已隔离、命令经 dashboard 审批,沙箱那层防护是冗余的,
|
|
622
|
+
// 这里拉满。详见 docs/codex-sandbox-network-blocked.md。
|
|
623
|
+
sandbox: "danger-full-access",
|
|
624
|
+
config: null,
|
|
625
|
+
baseInstructions: null,
|
|
626
|
+
developerInstructions,
|
|
627
|
+
compactPrompt: null,
|
|
628
|
+
includeApplyPatchTool: null,
|
|
629
|
+
experimentalRawEvents: false,
|
|
630
|
+
persistExtendedHistory: true,
|
|
631
|
+
}));
|
|
632
|
+
const id = extractThreadId(res);
|
|
633
|
+
if (!id)
|
|
634
|
+
throw new Error("codex thread/start returned no thread id");
|
|
635
|
+
return id;
|
|
636
|
+
}
|
|
637
|
+
function extractThreadId(result) {
|
|
638
|
+
if (!result || typeof result !== "object")
|
|
639
|
+
return "";
|
|
640
|
+
const r = result;
|
|
641
|
+
const thread = r.thread;
|
|
642
|
+
if (thread && typeof thread.id === "string")
|
|
643
|
+
return thread.id;
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Decide which session id to report. When resume was requested but codex emitted
|
|
648
|
+
* a fresh, different thread id AND the run failed, the resume did not land —
|
|
649
|
+
* return "" so the caller can retry fresh.
|
|
650
|
+
*/
|
|
651
|
+
function resolveSessionId(requestedResume, emitted, failed) {
|
|
652
|
+
if (failed && requestedResume && emitted && emitted !== requestedResume) {
|
|
653
|
+
return "";
|
|
654
|
+
}
|
|
655
|
+
return emitted;
|
|
656
|
+
}
|
|
657
|
+
// Strip common shell wrappers so the dashboard renders the user-meaningful
|
|
658
|
+
// command instead of `/bin/bash -lc '<actual>'`.
|
|
659
|
+
function prettyCommand(raw) {
|
|
660
|
+
let s = raw.trim();
|
|
661
|
+
const wrapper = /^(?:\/bin\/)?(?:ba|z)?sh\s+-lc\s+(['"])([\s\S]+)\1$/;
|
|
662
|
+
const m = s.match(wrapper);
|
|
663
|
+
if (m)
|
|
664
|
+
s = m[2].trim();
|
|
665
|
+
return s;
|
|
666
|
+
}
|
|
667
|
+
// ── Approval payload extraction ─────────────────────────────────────────
|
|
668
|
+
//
|
|
669
|
+
// Codex's app-server has shifted its exec/file approval params several times
|
|
670
|
+
// (top-level vs nested under `item`, command as string vs array). These
|
|
671
|
+
// helpers normalize the shapes we've seen into the worker-facing
|
|
672
|
+
// ApprovalRequestInput. When fields are missing we still build a usable
|
|
673
|
+
// summary so the human reviewer is never left with a blank card.
|
|
674
|
+
function extractExecApprovalInput(params) {
|
|
675
|
+
const source = params.item ?? params;
|
|
676
|
+
const rawCmd = source.command;
|
|
677
|
+
let command = "";
|
|
678
|
+
if (typeof rawCmd === "string") {
|
|
679
|
+
command = prettyCommand(rawCmd);
|
|
680
|
+
}
|
|
681
|
+
else if (Array.isArray(rawCmd)) {
|
|
682
|
+
command = prettyCommand(rawCmd.map((p) => String(p)).join(" "));
|
|
683
|
+
}
|
|
684
|
+
const cwd = typeof source.cwd === "string" ? source.cwd : undefined;
|
|
685
|
+
const reason = typeof source.reason === "string" ? source.reason : undefined;
|
|
686
|
+
const summary = command
|
|
687
|
+
? `请求执行命令:${command.length > 200 ? command.slice(0, 200) + "…" : command}`
|
|
688
|
+
: reason || "请求执行 shell 命令";
|
|
689
|
+
return { kind: "exec", summary, command: command || undefined, cwd };
|
|
690
|
+
}
|
|
691
|
+
function extractFileApprovalInput(params) {
|
|
692
|
+
const source = params.item ?? params;
|
|
693
|
+
const files = [];
|
|
694
|
+
const collect = (changes) => {
|
|
695
|
+
if (!Array.isArray(changes))
|
|
696
|
+
return;
|
|
697
|
+
for (const c of changes) {
|
|
698
|
+
if (!c)
|
|
699
|
+
continue;
|
|
700
|
+
if (typeof c === "string") {
|
|
701
|
+
files.push(c);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (typeof c === "object") {
|
|
705
|
+
const rec = c;
|
|
706
|
+
const p = (rec.path ?? rec.file ?? rec.targetPath);
|
|
707
|
+
if (typeof p === "string")
|
|
708
|
+
files.push(p);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
collect(source.changes);
|
|
713
|
+
collect(source.files);
|
|
714
|
+
collect(source.patch?.changes);
|
|
715
|
+
const summary = files.length
|
|
716
|
+
? `请求修改文件:${files.slice(0, 3).join("、")}${files.length > 3 ? `(共 ${files.length} 项)` : ""}`
|
|
717
|
+
: "请求修改文件";
|
|
718
|
+
return { kind: "file_change", summary, files: files.length ? files : undefined };
|
|
719
|
+
}
|