@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.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/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +4 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +64 -4
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
- package/dist/cc-orchestrator/external-agent-driver.js +202 -38
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +96 -13
- package/dist/credentials/access.d.ts +1 -0
- package/dist/credentials/access.js +2 -0
- package/dist/credentials/index.d.ts +2 -1
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/oauth.d.ts +20 -0
- package/dist/credentials/oauth.js +114 -0
- package/dist/credentials/store.d.ts +1 -0
- package/dist/credentials/store.js +3 -1
- package/dist/credentials/types.d.ts +47 -1
- package/dist/engine/engine.d.ts +6 -2
- package/dist/engine/engine.js +159 -192
- package/dist/engine/goal.d.ts +17 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/input-attachments.js +156 -13
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +30 -1
- package/dist/engine/turn-loop.js +112 -17
- package/dist/hooks/goal-stop-hook.d.ts +33 -1
- package/dist/hooks/goal-stop-hook.js +202 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/preset/index.js +14 -4
- package/dist/protocol/server.js +1 -1
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-manager.js +34 -1
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +25 -5
- package/dist/tool-system/builtin/background-jobs.js +105 -7
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
- package/dist/tool-system/builtin/drive-claude-code.js +307 -20
- package/dist/tool-system/builtin/index.js +15 -3
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/context.d.ts +18 -0
- package/dist/tool-system/mcp-manager.d.ts +14 -2
- package/dist/tool-system/mcp-manager.js +56 -7
- package/dist/types.d.ts +23 -7
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions";
|
|
|
2
2
|
export interface BuildArgsOpts {
|
|
3
3
|
prompt: string;
|
|
4
4
|
resumeSessionId?: string;
|
|
5
|
+
/** Optional caller-specified model override. Passed through without validation; the CLI reports unknown models. */
|
|
6
|
+
model?: string;
|
|
5
7
|
permissionMode: PermissionMode;
|
|
6
8
|
cwd: string;
|
|
7
9
|
imagePaths?: string[];
|
|
@@ -14,6 +14,8 @@ export const claudeAdapter = {
|
|
|
14
14
|
buildArgs(opts) {
|
|
15
15
|
// -p (print/headless) + stream-json REQUIRES --verbose (verified).
|
|
16
16
|
const args = ["-p", opts.prompt, "--output-format", "stream-json", "--verbose"];
|
|
17
|
+
if (opts.model)
|
|
18
|
+
args.push("--model", opts.model);
|
|
17
19
|
if (opts.resumeSessionId)
|
|
18
20
|
args.push("--resume", opts.resumeSessionId);
|
|
19
21
|
// Hard-disallow Workflow: driving CC unattended (esp. bypassPermissions),
|
|
@@ -82,6 +84,8 @@ export const codexAdapter = {
|
|
|
82
84
|
else {
|
|
83
85
|
args.push("--sandbox", opts.permissionMode === "acceptEdits" ? "workspace-write" : "read-only");
|
|
84
86
|
}
|
|
87
|
+
if (opts.model)
|
|
88
|
+
args.push("--model", opts.model);
|
|
85
89
|
if (opts.codexImageInputSupported) {
|
|
86
90
|
for (const imagePath of opts.imagePaths ?? [])
|
|
87
91
|
args.push("-i", imagePath);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HistoryMessage } from "./session-history.js";
|
|
1
|
+
import type { HistoryMessage, SessionTailEvent } from "./session-history.js";
|
|
2
2
|
/**
|
|
3
3
|
* Read the last `limit` user/assistant messages from a codex CLI session,
|
|
4
4
|
* returning the SAME shape as the claude-side `readRecentHistory` so the room
|
|
@@ -23,3 +23,16 @@ export declare function readCodexRecentHistory(cwd: string, threadId: string, li
|
|
|
23
23
|
hasMore: boolean;
|
|
24
24
|
totalCount: number;
|
|
25
25
|
};
|
|
26
|
+
/** Parse a bounded/raw Codex rollout snapshot. Shared with the desktop tail
|
|
27
|
+
* follower so its initial history and EOF cursor are atomic. */
|
|
28
|
+
export declare function parseCodexRecentHistory(raw: string, limit: number): {
|
|
29
|
+
messages: HistoryMessage[];
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
totalCount: number;
|
|
32
|
+
};
|
|
33
|
+
/** Parse one newly-appended Codex rollout JSONL line. `response_item` is the
|
|
34
|
+
* authoritative rendered stream; parallel `event_msg.agent_message` records
|
|
35
|
+
* are deliberately ignored to avoid duplicate assistant bubbles. */
|
|
36
|
+
export declare function parseCodexTranscriptLine(line: string): SessionTailEvent[];
|
|
37
|
+
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`. */
|
|
38
|
+
export declare function findCodexRolloutFile(codexHome: string, cwd: string, threadId: string): string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync, existsSync, openSync, readSync, closeSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync, openSync, readSync, closeSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
/**
|
|
@@ -22,7 +22,7 @@ import { homedir } from "node:os";
|
|
|
22
22
|
*/
|
|
23
23
|
export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(homedir(), ".codex")) {
|
|
24
24
|
const empty = { messages: [], hasMore: false, totalCount: 0 };
|
|
25
|
-
const file =
|
|
25
|
+
const file = findCodexRolloutFile(codexHome, cwd, threadId);
|
|
26
26
|
if (!file)
|
|
27
27
|
return empty;
|
|
28
28
|
let raw;
|
|
@@ -32,6 +32,11 @@ export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(ho
|
|
|
32
32
|
catch {
|
|
33
33
|
return empty;
|
|
34
34
|
}
|
|
35
|
+
return parseCodexRecentHistory(raw, limit);
|
|
36
|
+
}
|
|
37
|
+
/** Parse a bounded/raw Codex rollout snapshot. Shared with the desktop tail
|
|
38
|
+
* follower so its initial history and EOF cursor are atomic. */
|
|
39
|
+
export function parseCodexRecentHistory(raw, limit) {
|
|
35
40
|
const all = [];
|
|
36
41
|
for (const line of raw.split("\n")) {
|
|
37
42
|
if (!line.trim())
|
|
@@ -55,7 +60,11 @@ export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(ho
|
|
|
55
60
|
all.push({ role: p.role, text: t });
|
|
56
61
|
}
|
|
57
62
|
else if (p.type === "function_call" || p.type === "custom_tool_call") {
|
|
58
|
-
const tool = {
|
|
63
|
+
const tool = {
|
|
64
|
+
name: typeof p.name === "string" ? p.name : "tool",
|
|
65
|
+
summary: summaryOf(p),
|
|
66
|
+
args: argsOf(p),
|
|
67
|
+
};
|
|
59
68
|
const last = all[all.length - 1];
|
|
60
69
|
// Attach the tool to the preceding assistant turn; otherwise start one.
|
|
61
70
|
if (last && last.role === "assistant") {
|
|
@@ -70,6 +79,57 @@ export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(ho
|
|
|
70
79
|
const start = Math.max(0, all.length - lim);
|
|
71
80
|
return { messages: all.slice(start), hasMore: start > 0, totalCount: all.length };
|
|
72
81
|
}
|
|
82
|
+
/** Parse one newly-appended Codex rollout JSONL line. `response_item` is the
|
|
83
|
+
* authoritative rendered stream; parallel `event_msg.agent_message` records
|
|
84
|
+
* are deliberately ignored to avoid duplicate assistant bubbles. */
|
|
85
|
+
export function parseCodexTranscriptLine(line) {
|
|
86
|
+
let d;
|
|
87
|
+
try {
|
|
88
|
+
d = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
if (d?.type === "event_msg" && d.payload?.type === "task_complete") {
|
|
94
|
+
return [{ type: "turn_end", reason: "completed" }];
|
|
95
|
+
}
|
|
96
|
+
if (d?.type !== "response_item" || !d.payload)
|
|
97
|
+
return [];
|
|
98
|
+
const p = d.payload;
|
|
99
|
+
if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
|
|
100
|
+
const text = textOf(p.content).trim();
|
|
101
|
+
if (!text || text.startsWith("<environment_context>"))
|
|
102
|
+
return [];
|
|
103
|
+
return [{ type: p.role === "user" ? "user" : "assistant", text }];
|
|
104
|
+
}
|
|
105
|
+
if (p.type === "function_call" || p.type === "custom_tool_call") {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
type: "tool",
|
|
109
|
+
id: typeof p.call_id === "string" ? p.call_id : undefined,
|
|
110
|
+
name: typeof p.name === "string" ? p.name : "tool",
|
|
111
|
+
summary: summaryOf(p),
|
|
112
|
+
args: argsOf(p),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
if (p.type === "function_call_output" || p.type === "custom_tool_call_output") {
|
|
117
|
+
const result = typeof p.output === "string"
|
|
118
|
+
? p.output
|
|
119
|
+
: typeof p.content === "string"
|
|
120
|
+
? p.content
|
|
121
|
+
: JSON.stringify(p.output ?? p.content ?? "");
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: "tool_result",
|
|
125
|
+
id: typeof p.call_id === "string" ? p.call_id : undefined,
|
|
126
|
+
result: result.slice(0, 4000),
|
|
127
|
+
isError: Boolean(p.is_error),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
73
133
|
/** Join the text of codex content parts (`input_text`/`output_text`). */
|
|
74
134
|
function textOf(content) {
|
|
75
135
|
if (typeof content === "string")
|
|
@@ -121,7 +181,7 @@ function argsOf(payload) {
|
|
|
121
181
|
return undefined;
|
|
122
182
|
}
|
|
123
183
|
/** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`. */
|
|
124
|
-
function
|
|
184
|
+
export function findCodexRolloutFile(codexHome, cwd, threadId) {
|
|
125
185
|
const root = join(codexHome, "sessions");
|
|
126
186
|
if (!existsSync(root))
|
|
127
187
|
return undefined;
|
|
@@ -2,6 +2,7 @@ import { readFileSync, existsSync, readdirSync, statSync, openSync, readSync, cl
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { encodeCwd } from "./session-discovery.js";
|
|
5
|
+
import { logger } from "../logging/logger.js";
|
|
5
6
|
/**
|
|
6
7
|
* #6 — attribute file changes made by a background external agent (DriveAgent
|
|
7
8
|
* running `claude` / `codex`). Those Edit/Write calls land in the external CLI's
|
|
@@ -143,11 +144,27 @@ export function readCodexChangedFiles(cwd, threadId, codexHome = join(homedir(),
|
|
|
143
144
|
/** Dispatch by CLI. Unknown cli → []. Never throws. */
|
|
144
145
|
export function readExternalChangedFiles(cli, cwd, sessionId) {
|
|
145
146
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const files = cli === "codex"
|
|
148
|
+
? readCodexChangedFiles(cwd, sessionId)
|
|
149
|
+
: readClaudeChangedFiles(cwd, sessionId);
|
|
150
|
+
logger.debug("changed_files.external.extracted", {
|
|
151
|
+
cat: "changed_files",
|
|
152
|
+
cli,
|
|
153
|
+
cwd,
|
|
154
|
+
externalSessionId: sessionId,
|
|
155
|
+
size: files.length,
|
|
156
|
+
files,
|
|
157
|
+
});
|
|
158
|
+
return files;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
logger.debug("changed_files.external.extract_failed", {
|
|
162
|
+
cat: "changed_files",
|
|
163
|
+
cli,
|
|
164
|
+
cwd,
|
|
165
|
+
externalSessionId: sessionId,
|
|
166
|
+
error: err instanceof Error ? err.message : String(err),
|
|
167
|
+
});
|
|
151
168
|
return [];
|
|
152
169
|
}
|
|
153
170
|
}
|
|
@@ -11,7 +11,7 @@ export declare function runWithLines(adapter: AgentAdapter, lines: string[], exi
|
|
|
11
11
|
export interface DriverRunOpts extends Omit<BuildArgsOpts, "permissionMode"> {
|
|
12
12
|
permissionMode?: PermissionMode;
|
|
13
13
|
}
|
|
14
|
-
export declare function detectCodexImageInput(command: string, cwd: string): Promise<boolean>;
|
|
14
|
+
export declare function detectCodexImageInput(command: string, cwd: string, signal?: AbortSignal): Promise<boolean>;
|
|
15
15
|
/** Spawn ONE headless agent run, collect stream-json to exit, return result.
|
|
16
16
|
* No time concept — a single turn. Honors AbortSignal (kills the child). */
|
|
17
17
|
export declare function runAgentOnce(adapter: AgentAdapter, opts: DriverRunOpts & {
|
|
@@ -1,17 +1,130 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { pathWithCommonBins } from "./cc-capability.js";
|
|
4
|
+
import { killProcessGroup } from "../runtime/spawn-common.js";
|
|
4
5
|
/** Pure: reduce collected output lines + exit code to a result. Unit-testable. */
|
|
5
6
|
export function runWithLines(adapter, lines, exitCode) {
|
|
6
7
|
const parsed = adapter.parseResult(lines);
|
|
7
8
|
return { ...parsed, exitCode, lines };
|
|
8
9
|
}
|
|
9
10
|
const codexImageFlagCache = new Map();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const CODEX_IMAGE_PROBE_TIMEOUT_MS = 5_000;
|
|
12
|
+
const AGENT_TERMINATE_GRACE_MS = 500;
|
|
13
|
+
function listPosixProcessTree(rootPid) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
execFile("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" }, (err, stdout) => {
|
|
16
|
+
if (err) {
|
|
17
|
+
resolve([rootPid]);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const childrenByParent = new Map();
|
|
21
|
+
for (const line of stdout.split("\n")) {
|
|
22
|
+
const [pidText, parentText] = line.trim().split(/\s+/, 2);
|
|
23
|
+
const pid = Number(pidText);
|
|
24
|
+
const parentPid = Number(parentText);
|
|
25
|
+
if (!Number.isInteger(pid) || !Number.isInteger(parentPid))
|
|
26
|
+
continue;
|
|
27
|
+
const children = childrenByParent.get(parentPid) ?? [];
|
|
28
|
+
children.push(pid);
|
|
29
|
+
childrenByParent.set(parentPid, children);
|
|
30
|
+
}
|
|
31
|
+
const pids = [];
|
|
32
|
+
const visit = (pid) => {
|
|
33
|
+
if (pids.includes(pid))
|
|
34
|
+
return;
|
|
35
|
+
pids.push(pid);
|
|
36
|
+
for (const childPid of childrenByParent.get(pid) ?? [])
|
|
37
|
+
visit(childPid);
|
|
38
|
+
};
|
|
39
|
+
visit(rootPid);
|
|
40
|
+
resolve(pids);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isPidAlive(pid) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function delay(ms) {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
async function waitForPidsToExit(pids, timeoutMs) {
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
while ([...pids].some(isPidAlive)) {
|
|
59
|
+
if (Date.now() >= deadline)
|
|
60
|
+
return false;
|
|
61
|
+
await delay(25);
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function signalPids(pids, killSignal) {
|
|
66
|
+
for (const pid of pids) {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, killSignal);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// already gone
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function terminateAttachedProcessTree(child) {
|
|
76
|
+
const pid = child.pid;
|
|
77
|
+
if (!pid) {
|
|
78
|
+
try {
|
|
79
|
+
child.kill("SIGKILL");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// already gone
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
// Windows has no POSIX SIGTERM semantics or negative-pid groups. Reap the
|
|
88
|
+
// tree via taskkill, then use positive-pid process.kill as the fallback the
|
|
89
|
+
// platform supports when taskkill is unavailable/races.
|
|
90
|
+
await killProcessGroup(pid, { graceMs: AGENT_TERMINATE_GRACE_MS });
|
|
91
|
+
if (isPidAlive(pid)) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// already gone
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const trackedPids = new Set(await listPosixProcessTree(pid));
|
|
102
|
+
signalPids(trackedPids, "SIGTERM");
|
|
103
|
+
if (await waitForPidsToExit(trackedPids, AGENT_TERMINATE_GRACE_MS))
|
|
104
|
+
return;
|
|
105
|
+
// Capture descendants created during the grace window while the leader is
|
|
106
|
+
// still addressable, but retain the first snapshot so reparented stubborn
|
|
107
|
+
// descendants cannot disappear from the escalation set.
|
|
108
|
+
for (const treePid of await listPosixProcessTree(pid))
|
|
109
|
+
trackedPids.add(treePid);
|
|
110
|
+
signalPids(trackedPids, "SIGKILL");
|
|
111
|
+
await waitForPidsToExit(trackedPids, 1_000);
|
|
112
|
+
}
|
|
113
|
+
function abortError() {
|
|
114
|
+
const err = new Error("Agent run aborted");
|
|
115
|
+
err.name = "AbortError";
|
|
116
|
+
return err;
|
|
117
|
+
}
|
|
118
|
+
function throwIfAborted(signal) {
|
|
119
|
+
if (signal?.aborted)
|
|
120
|
+
throw abortError();
|
|
121
|
+
}
|
|
122
|
+
function createCodexImageProbe(command, cwd, signal) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
if (signal?.aborted) {
|
|
125
|
+
reject(abortError());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
15
128
|
const child = spawn(command, ["exec", "--help"], {
|
|
16
129
|
cwd,
|
|
17
130
|
env: { ...process.env, PATH: pathWithCommonBins() },
|
|
@@ -20,31 +133,65 @@ export function detectCodexImageInput(command, cwd) {
|
|
|
20
133
|
});
|
|
21
134
|
let out = "";
|
|
22
135
|
let settled = false;
|
|
136
|
+
let terminating = false;
|
|
137
|
+
const cleanup = () => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
signal?.removeEventListener("abort", onAbort);
|
|
140
|
+
};
|
|
23
141
|
const done = (value) => {
|
|
24
142
|
if (settled)
|
|
25
143
|
return;
|
|
26
144
|
settled = true;
|
|
27
|
-
|
|
145
|
+
cleanup();
|
|
28
146
|
resolve(value);
|
|
29
147
|
};
|
|
148
|
+
const fail = (err) => {
|
|
149
|
+
if (settled)
|
|
150
|
+
return;
|
|
151
|
+
settled = true;
|
|
152
|
+
cleanup();
|
|
153
|
+
reject(err);
|
|
154
|
+
};
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
if (settled || terminating)
|
|
157
|
+
return;
|
|
158
|
+
terminating = true;
|
|
159
|
+
cleanup();
|
|
160
|
+
void terminateAttachedProcessTree(child).then(() => fail(abortError()));
|
|
161
|
+
};
|
|
30
162
|
const timer = setTimeout(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
163
|
+
if (settled || terminating)
|
|
164
|
+
return;
|
|
165
|
+
terminating = true;
|
|
166
|
+
cleanup();
|
|
167
|
+
void terminateAttachedProcessTree(child).then(() => done(false));
|
|
168
|
+
}, CODEX_IMAGE_PROBE_TIMEOUT_MS);
|
|
169
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
170
|
+
if (signal?.aborted)
|
|
171
|
+
onAbort();
|
|
39
172
|
child.stdout?.on("data", (chunk) => {
|
|
40
173
|
out += chunk.toString("utf8");
|
|
41
174
|
});
|
|
42
175
|
child.stderr?.on("data", (chunk) => {
|
|
43
176
|
out += chunk.toString("utf8");
|
|
44
177
|
});
|
|
45
|
-
child.on("error", () =>
|
|
46
|
-
|
|
178
|
+
child.on("error", () => {
|
|
179
|
+
if (!terminating)
|
|
180
|
+
done(false);
|
|
181
|
+
});
|
|
182
|
+
child.on("exit", () => {
|
|
183
|
+
if (!terminating)
|
|
184
|
+
done(/(?:^|\s)-i(?:,|\s)|--image\b/.test(out));
|
|
185
|
+
});
|
|
47
186
|
});
|
|
187
|
+
}
|
|
188
|
+
export function detectCodexImageInput(command, cwd, signal) {
|
|
189
|
+
if (signal)
|
|
190
|
+
return createCodexImageProbe(command, cwd, signal);
|
|
191
|
+
const cached = codexImageFlagCache.get(command);
|
|
192
|
+
if (cached)
|
|
193
|
+
return cached;
|
|
194
|
+
const probe = createCodexImageProbe(command, cwd);
|
|
48
195
|
codexImageFlagCache.set(command, probe);
|
|
49
196
|
return probe;
|
|
50
197
|
}
|
|
@@ -53,17 +200,25 @@ export function detectCodexImageInput(command, cwd) {
|
|
|
53
200
|
export function runAgentOnce(adapter, opts, signal) {
|
|
54
201
|
return new Promise((resolve, reject) => {
|
|
55
202
|
void (async () => {
|
|
203
|
+
throwIfAborted(signal);
|
|
56
204
|
const codexImageInputSupported = adapter.kind === "codex" && (opts.imagePaths?.length ?? 0) > 0
|
|
57
|
-
? await detectCodexImageInput(opts.command, opts.cwd).catch(() =>
|
|
205
|
+
? await detectCodexImageInput(opts.command, opts.cwd, signal).catch((err) => {
|
|
206
|
+
if (signal?.aborted || err?.name === "AbortError")
|
|
207
|
+
throw err;
|
|
208
|
+
return false;
|
|
209
|
+
})
|
|
58
210
|
: false;
|
|
211
|
+
throwIfAborted(signal);
|
|
59
212
|
const args = adapter.buildArgs({
|
|
60
213
|
prompt: opts.prompt,
|
|
61
214
|
resumeSessionId: opts.resumeSessionId,
|
|
215
|
+
model: opts.model,
|
|
62
216
|
permissionMode: opts.permissionMode ?? "default",
|
|
63
217
|
cwd: opts.cwd,
|
|
64
218
|
imagePaths: opts.imagePaths,
|
|
65
219
|
codexImageInputSupported,
|
|
66
220
|
});
|
|
221
|
+
throwIfAborted(signal);
|
|
67
222
|
// claude takes the prompt in argv (`-p <prompt>`) and wants stdin closed
|
|
68
223
|
// (verified: avoids a 3s wait). codex `exec` reads the prompt from stdin
|
|
69
224
|
// (argv ends with `-`), so adapters that set promptViaStdin get a piped
|
|
@@ -72,14 +227,27 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
72
227
|
const child = spawn(opts.command, args, {
|
|
73
228
|
cwd: opts.cwd,
|
|
74
229
|
env: { ...process.env, PATH: pathWithCommonBins() },
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// fired (the "后台任务没返回" bug). Bound to the worker, it lives or dies
|
|
79
|
-
// with the process that's actually listening for its result.
|
|
230
|
+
// Keep the agent in the owning worker/app process group. If that owner
|
|
231
|
+
// is force-terminated, the CLI and its descendants must not escape into
|
|
232
|
+
// a detached group that can keep editing the workspace unseen.
|
|
80
233
|
detached: false,
|
|
81
234
|
stdio: [viaStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
82
235
|
});
|
|
236
|
+
let settled = false;
|
|
237
|
+
let abortRequested = false;
|
|
238
|
+
let termination;
|
|
239
|
+
const cleanup = () => {
|
|
240
|
+
signal?.removeEventListener("abort", onAbort);
|
|
241
|
+
};
|
|
242
|
+
const onAbort = () => {
|
|
243
|
+
if (abortRequested)
|
|
244
|
+
return;
|
|
245
|
+
abortRequested = true;
|
|
246
|
+
termination = terminateAttachedProcessTree(child);
|
|
247
|
+
};
|
|
248
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
249
|
+
if (signal?.aborted)
|
|
250
|
+
onAbort();
|
|
83
251
|
if (viaStdin && child.stdin) {
|
|
84
252
|
child.stdin.end(opts.prompt);
|
|
85
253
|
}
|
|
@@ -88,20 +256,11 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
88
256
|
const rl = createInterface({ input: child.stdout });
|
|
89
257
|
rl.on("line", (line) => lines.push(line));
|
|
90
258
|
}
|
|
91
|
-
// Not detached → no own process group, so kill the child directly (a
|
|
92
|
-
// negative-pid group kill would target the worker's group). claude has no
|
|
93
|
-
// long-lived child tree of its own here, so a direct SIGTERM is sufficient.
|
|
94
|
-
const onAbort = () => {
|
|
95
|
-
try {
|
|
96
|
-
child.kill("SIGTERM");
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
/* already gone */
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
259
|
child.on("error", (err) => {
|
|
104
|
-
|
|
260
|
+
if (settled)
|
|
261
|
+
return;
|
|
262
|
+
settled = true;
|
|
263
|
+
cleanup();
|
|
105
264
|
// A missing binary is the most common failure (user hasn't installed the
|
|
106
265
|
// CLI, or GUI-launched Electron's PATH misses it). Turn the cryptic
|
|
107
266
|
// "spawn codex ENOENT" into something actionable that names the command.
|
|
@@ -112,8 +271,13 @@ export function runAgentOnce(adapter, opts, signal) {
|
|
|
112
271
|
reject(err);
|
|
113
272
|
});
|
|
114
273
|
child.on("exit", (code) => {
|
|
115
|
-
|
|
116
|
-
|
|
274
|
+
if (settled)
|
|
275
|
+
return;
|
|
276
|
+
settled = true;
|
|
277
|
+
cleanup();
|
|
278
|
+
void (termination ?? Promise.resolve()).then(() => {
|
|
279
|
+
resolve(runWithLines(adapter, lines, code));
|
|
280
|
+
});
|
|
117
281
|
});
|
|
118
282
|
})().catch(reject);
|
|
119
283
|
});
|
|
@@ -11,6 +11,41 @@ export interface HistoryMessage {
|
|
|
11
11
|
}[];
|
|
12
12
|
ts?: number;
|
|
13
13
|
}
|
|
14
|
+
/** One render-relevant event parsed from an appended external-CLI transcript
|
|
15
|
+
* line. Kept CLI-neutral so the desktop room follower can feed Claude Code and
|
|
16
|
+
* Codex through the same RoomManager push path. */
|
|
17
|
+
export type SessionTailEvent = {
|
|
18
|
+
type: "user";
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "assistant";
|
|
22
|
+
text: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "tool";
|
|
25
|
+
id?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
summary: string;
|
|
28
|
+
args?: Record<string, unknown>;
|
|
29
|
+
} | {
|
|
30
|
+
type: "tool_result";
|
|
31
|
+
id?: string;
|
|
32
|
+
result: string;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
type: "turn_end";
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
38
|
+
/** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
|
|
39
|
+
* follower can take its initial snapshot and EOF cursor from the same read,
|
|
40
|
+
* eliminating the snapshot→subscribe race. */
|
|
41
|
+
export declare function parseRecentHistory(raw: string, limit: number): {
|
|
42
|
+
messages: HistoryMessage[];
|
|
43
|
+
hasMore: boolean;
|
|
44
|
+
totalCount: number;
|
|
45
|
+
};
|
|
46
|
+
/** Parse one newly-appended Claude Code transcript JSONL line into the compact
|
|
47
|
+
* event vocabulary consumed by the desktop room follower. */
|
|
48
|
+
export declare function parseClaudeTranscriptLine(line: string): SessionTailEvent[];
|
|
14
49
|
/** Read the last `limit` user/assistant messages from a claude session jsonl. */
|
|
15
50
|
export declare function readRecentHistory(cwd: string, sessionId: string, limit: number, claudeHome?: string): {
|
|
16
51
|
messages: HistoryMessage[];
|