@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.1
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/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- 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/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +11 -1
- package/dist/credentials/access.js +77 -1
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/oauth.d.ts +25 -0
- package/dist/credentials/oauth.js +179 -0
- package/dist/credentials/store.d.ts +2 -1
- package/dist/credentials/store.js +19 -8
- package/dist/credentials/types.d.ts +84 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +67 -34
- package/dist/engine/engine.js +448 -247
- package/dist/engine/goal.d.ts +19 -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/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +48 -1
- package/dist/engine/turn-loop.js +307 -37
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +44 -2
- package/dist/hooks/goal-stop-hook.js +775 -52
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/preset/index.js +14 -4
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +291 -73
- package/dist/protocol/types.d.ts +36 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +396 -27
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- 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/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
- package/dist/tool-system/builtin/background-jobs.js +109 -7
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- 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/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +60 -5
- 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/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +35 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +20 -2
- package/dist/tool-system/mcp-manager.js +111 -12
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +38 -7
- package/package.json +1 -1
|
@@ -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[];
|
|
@@ -19,23 +19,28 @@ function toolsOf(content) {
|
|
|
19
19
|
const inp = p.input ?? {};
|
|
20
20
|
const summary = inp.command ?? inp.file_path ?? inp.path ?? inp.url ?? inp.pattern ?? inp.query ?? "";
|
|
21
21
|
const args = inp && typeof inp === "object" && Object.keys(inp).length > 0 ? inp : undefined;
|
|
22
|
-
out.push({
|
|
22
|
+
out.push({
|
|
23
|
+
name: typeof p.name === "string" ? p.name : "tool",
|
|
24
|
+
summary: String(summary).slice(0, 120),
|
|
25
|
+
args,
|
|
26
|
+
});
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
return out;
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
function toolResultText(content) {
|
|
32
|
+
if (typeof content === "string")
|
|
33
|
+
return content;
|
|
34
|
+
if (!Array.isArray(content))
|
|
35
|
+
return "";
|
|
36
|
+
return content
|
|
37
|
+
.map((part) => typeof part === "string" ? part : typeof part?.text === "string" ? part.text : "")
|
|
38
|
+
.join("");
|
|
39
|
+
}
|
|
40
|
+
/** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
|
|
41
|
+
* follower can take its initial snapshot and EOF cursor from the same read,
|
|
42
|
+
* eliminating the snapshot→subscribe race. */
|
|
43
|
+
export function parseRecentHistory(raw, limit) {
|
|
39
44
|
const all = [];
|
|
40
45
|
for (const line of raw.split("\n")) {
|
|
41
46
|
if (!line.trim())
|
|
@@ -65,3 +70,81 @@ export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homed
|
|
|
65
70
|
const start = Math.max(0, all.length - lim);
|
|
66
71
|
return { messages: all.slice(start), hasMore: start > 0, totalCount: all.length };
|
|
67
72
|
}
|
|
73
|
+
/** Parse one newly-appended Claude Code transcript JSONL line into the compact
|
|
74
|
+
* event vocabulary consumed by the desktop room follower. */
|
|
75
|
+
export function parseClaudeTranscriptLine(line) {
|
|
76
|
+
let d;
|
|
77
|
+
try {
|
|
78
|
+
d = JSON.parse(line);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out = [];
|
|
84
|
+
if (d.type === "user") {
|
|
85
|
+
const content = d.message?.content;
|
|
86
|
+
if (Array.isArray(content)) {
|
|
87
|
+
for (const part of content) {
|
|
88
|
+
if (part?.type !== "tool_result")
|
|
89
|
+
continue;
|
|
90
|
+
out.push({
|
|
91
|
+
type: "tool_result",
|
|
92
|
+
id: typeof part.tool_use_id === "string" ? part.tool_use_id : undefined,
|
|
93
|
+
result: toolResultText(part.content).slice(0, 4000),
|
|
94
|
+
isError: Boolean(part.is_error),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const text = textOf(content).trim();
|
|
99
|
+
if (text && !NOISE.some((noise) => text.startsWith(noise))) {
|
|
100
|
+
out.unshift({ type: "user", text });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
if (d.type === "assistant" && Array.isArray(d.message?.content)) {
|
|
105
|
+
for (const part of d.message.content) {
|
|
106
|
+
if (part?.type === "text" && typeof part.text === "string" && part.text) {
|
|
107
|
+
out.push({ type: "assistant", text: part.text });
|
|
108
|
+
}
|
|
109
|
+
else if (part?.type === "tool_use") {
|
|
110
|
+
const input = part.input && typeof part.input === "object"
|
|
111
|
+
? part.input
|
|
112
|
+
: undefined;
|
|
113
|
+
out.push({
|
|
114
|
+
type: "tool",
|
|
115
|
+
id: typeof part.id === "string" ? part.id : undefined,
|
|
116
|
+
name: typeof part.name === "string" ? part.name : "tool",
|
|
117
|
+
summary: toolsOf([part])[0]?.summary ?? "",
|
|
118
|
+
args: input,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (d.message.stop_reason === "end_turn") {
|
|
123
|
+
out.push({ type: "turn_end", reason: "completed" });
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
if (d.type === "result") {
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
type: "turn_end",
|
|
131
|
+
reason: typeof d.subtype === "string" ? d.subtype : "completed",
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
/** Read the last `limit` user/assistant messages from a claude session jsonl. */
|
|
138
|
+
export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homedir(), ".claude")) {
|
|
139
|
+
const file = join(claudeHome, "projects", encodeCwd(cwd), `${sessionId}.jsonl`);
|
|
140
|
+
if (!existsSync(file))
|
|
141
|
+
return { messages: [], hasMore: false, totalCount: 0 };
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = readFileSync(file, "utf-8");
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { messages: [], hasMore: false, totalCount: 0 };
|
|
148
|
+
}
|
|
149
|
+
return parseRecentHistory(raw, limit);
|
|
150
|
+
}
|
|
@@ -33,6 +33,8 @@ import { createRunManager } from "../run/factory.js";
|
|
|
33
33
|
import { startAutomation } from "../automation/index.js";
|
|
34
34
|
import { CronStore, defaultCronStorePath } from "../automation/store.js";
|
|
35
35
|
import { resolveLLMConfigForTag } from "../engine/resolve-llm-config.js";
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { getApprovalRouter } from "../tool-system/permission.js";
|
|
36
38
|
const cwd = process.env.AGENT_CWD ?? process.cwd();
|
|
37
39
|
const port = Number(process.env.AGENT_TCP_PORT ?? "4321");
|
|
38
40
|
const host = process.env.AGENT_TCP_HOST ?? "127.0.0.1";
|
|
@@ -113,9 +115,18 @@ const automation = startAutomation({
|
|
|
113
115
|
// ── Serve over TCP ──────────────────────────────────────────────
|
|
114
116
|
// One AgentServer per accepted connection, all sharing the same chatManager.
|
|
115
117
|
const servers = new Set();
|
|
116
|
-
listenTcp({ port, host }, (transport) => {
|
|
117
|
-
const server = new AgentServer({
|
|
118
|
+
listenTcp({ port, host }, (transport, socket) => {
|
|
119
|
+
const server = new AgentServer({
|
|
120
|
+
chatManager,
|
|
121
|
+
transport,
|
|
122
|
+
connectionId: randomUUID(),
|
|
123
|
+
approvalRouter: getApprovalRouter(),
|
|
124
|
+
});
|
|
118
125
|
servers.add(server);
|
|
126
|
+
socket.once("close", () => {
|
|
127
|
+
server.disconnect();
|
|
128
|
+
servers.delete(server);
|
|
129
|
+
});
|
|
119
130
|
})
|
|
120
131
|
.then((listener) => {
|
|
121
132
|
process.stderr.write(`[code-shell] automation server listening on ${host}:${listener.port}\n`);
|
|
@@ -30,7 +30,7 @@ export interface ContextManagerConfig {
|
|
|
30
30
|
* Async function type for LLM summarization calls.
|
|
31
31
|
* Injected by the Engine so the ContextManager doesn't depend on LLM directly.
|
|
32
32
|
*/
|
|
33
|
-
export type SummarizeFn = (prompt: string) => Promise<string>;
|
|
33
|
+
export type SummarizeFn = (prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
34
34
|
export type CompactStrategy = "micro" | "summary" | "window" | "snip" | "emergency";
|
|
35
35
|
export type OnCompactFn = (info: {
|
|
36
36
|
strategy: CompactStrategy;
|
|
@@ -111,7 +111,7 @@ export declare class ContextManager {
|
|
|
111
111
|
* Async context management — attempts LLM summarization before falling back.
|
|
112
112
|
* Call this when you have access to the LLM (between turns).
|
|
113
113
|
*/
|
|
114
|
-
manageAsync(messages: Message[]): Promise<Message[]>;
|
|
114
|
+
manageAsync(messages: Message[], signal?: AbortSignal): Promise<Message[]>;
|
|
115
115
|
/**
|
|
116
116
|
* Force maximum compaction, ignoring the ratio gates. This is what a manual
|
|
117
117
|
* `/compact` invokes: the user explicitly asked to shrink NOW, so we don't
|
|
@@ -126,7 +126,7 @@ export declare class ContextManager {
|
|
|
126
126
|
* a long-but-under-threshold text-only conversation (the /compact bug) still
|
|
127
127
|
* gets summarized here.
|
|
128
128
|
*/
|
|
129
|
-
forceSummarize(messages: Message[]): Promise<Message[]>;
|
|
129
|
+
forceSummarize(messages: Message[], signal?: AbortSignal): Promise<Message[]>;
|
|
130
130
|
/**
|
|
131
131
|
* Persist large tool_result blocks to disk and replace them with a
|
|
132
132
|
* preview + filepath. No-op when transcript path hasn't been set
|
package/dist/context/manager.js
CHANGED
|
@@ -162,7 +162,7 @@ export class ContextManager {
|
|
|
162
162
|
setSummarizeFn(fn) {
|
|
163
163
|
this.summarizeFn = fn;
|
|
164
164
|
}
|
|
165
|
-
async trySummaryCompact(messages, before, logEvent) {
|
|
165
|
+
async trySummaryCompact(messages, before, logEvent, signal) {
|
|
166
166
|
if (!this.summarizeFn || this.consecutiveSummaryFailures >= 3) {
|
|
167
167
|
return { messages, tokens: before, compacted: false, noProgress: false };
|
|
168
168
|
}
|
|
@@ -178,7 +178,7 @@ export class ContextManager {
|
|
|
178
178
|
// re-summarizes from scratch.
|
|
179
179
|
const priorSummary = extractAnchoredSummary(messages) ?? this.lastSummary;
|
|
180
180
|
const prompt = buildSummarizationPrompt(messagesToSummarize, priorSummary);
|
|
181
|
-
const summary = await this.summarizeFn(prompt);
|
|
181
|
+
const summary = await this.summarizeFn(prompt, signal);
|
|
182
182
|
if (!summary || summary.length <= 50) {
|
|
183
183
|
this.consecutiveSummaryFailures++;
|
|
184
184
|
logger.warn("context.summary_failed", {
|
|
@@ -358,7 +358,7 @@ export class ContextManager {
|
|
|
358
358
|
* Async context management — attempts LLM summarization before falling back.
|
|
359
359
|
* Call this when you have access to the LLM (between turns).
|
|
360
360
|
*/
|
|
361
|
-
async manageAsync(messages) {
|
|
361
|
+
async manageAsync(messages, signal) {
|
|
362
362
|
let result = messages;
|
|
363
363
|
// Tier 0a: Persist large tool_results to disk + replace with preview.
|
|
364
364
|
result = this.persistLargeToolResults(result);
|
|
@@ -428,7 +428,7 @@ export class ContextManager {
|
|
|
428
428
|
// Tier 2: LLM summary if approaching limit, or if micro was the only tier
|
|
429
429
|
// available in the 0.70-0.85 band and it freed nothing.
|
|
430
430
|
if (ratio >= this.config.compactAtRatio || shouldEscalateNoOpMicro) {
|
|
431
|
-
const summarized = await this.trySummaryCompact(result, tokens, "context.summary_compact");
|
|
431
|
+
const summarized = await this.trySummaryCompact(result, tokens, "context.summary_compact", signal);
|
|
432
432
|
result = summarized.messages;
|
|
433
433
|
tokens = summarized.tokens;
|
|
434
434
|
if (summarized.compacted) {
|
|
@@ -488,7 +488,7 @@ export class ContextManager {
|
|
|
488
488
|
* a long-but-under-threshold text-only conversation (the /compact bug) still
|
|
489
489
|
* gets summarized here.
|
|
490
490
|
*/
|
|
491
|
-
async forceSummarize(messages) {
|
|
491
|
+
async forceSummarize(messages, signal) {
|
|
492
492
|
let result = messages;
|
|
493
493
|
// Tier 0: same waste-removal + micro as the automatic path.
|
|
494
494
|
result = this.persistLargeToolResults(result);
|
|
@@ -505,7 +505,7 @@ export class ContextManager {
|
|
|
505
505
|
let tokens = this.estimateTokensHybrid(result);
|
|
506
506
|
// Unconditional LLM summary (no ratio gate).
|
|
507
507
|
{
|
|
508
|
-
const summarized = await this.trySummaryCompact(result, tokens, "context.force_summary_compact");
|
|
508
|
+
const summarized = await this.trySummaryCompact(result, tokens, "context.force_summary_compact", signal);
|
|
509
509
|
result = summarized.messages;
|
|
510
510
|
tokens = summarized.tokens;
|
|
511
511
|
if (summarized.compacted)
|
|
@@ -15,6 +15,9 @@ const RATIOS = {
|
|
|
15
15
|
mixed: 3.6, // Mixed content (typical for tool results)
|
|
16
16
|
cjk: 1.5, // CJK characters: ~1.5 chars per token
|
|
17
17
|
};
|
|
18
|
+
// Base64 is transport data, not text. A fixed vision estimate prevents large
|
|
19
|
+
// images from spuriously exhausting the text context window.
|
|
20
|
+
const IMAGE_TOKEN_ESTIMATE = 170;
|
|
18
21
|
/**
|
|
19
22
|
* Estimate token count for a string, detecting content type.
|
|
20
23
|
*/
|
|
@@ -80,12 +83,11 @@ function estimateBlockTokens(block) {
|
|
|
80
83
|
if (block.name)
|
|
81
84
|
tokens += estimateStringTokens(block.name);
|
|
82
85
|
if (block.source?.data) {
|
|
83
|
-
tokens +=
|
|
84
|
-
tokens += estimateStringTokens(block.source.data);
|
|
86
|
+
tokens += IMAGE_TOKEN_ESTIMATE;
|
|
85
87
|
}
|
|
86
88
|
const maybeOpenAI = block;
|
|
87
89
|
if (typeof maybeOpenAI.image_url?.url === "string") {
|
|
88
|
-
tokens +=
|
|
90
|
+
tokens += IMAGE_TOKEN_ESTIMATE;
|
|
89
91
|
}
|
|
90
92
|
return tokens;
|
|
91
93
|
}
|