@cjhyy/code-shell-capability-coding 0.8.8 → 0.8.10
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/external-agent-session-store.d.ts +3 -1
- package/dist/cc-orchestrator/external-agent-session-store.js +110 -71
- package/dist/external-runtimes/claude-code/event-translator.js +9 -5
- package/dist/index.capability.js +4 -3
- package/dist/tools/drive-agent.d.ts +1 -1
- package/dist/tools/drive-agent.js +155 -67
- package/package.json +4 -3
|
@@ -22,14 +22,16 @@ export type ExternalAgentSessionRecord = Omit<ExternalAgentSessionBinding, "crea
|
|
|
22
22
|
export declare function defaultExternalAgentSessionStorePath(): string;
|
|
23
23
|
export declare class ExternalAgentSessionStore {
|
|
24
24
|
private readonly file;
|
|
25
|
+
private readonly pendingBindings;
|
|
25
26
|
constructor(file?: string);
|
|
26
27
|
/** Snapshot all known bindings. Returned objects are detached from the
|
|
27
28
|
* persisted array so read-only consumers (for example room discovery) can
|
|
28
29
|
* correlate worktree sessions without gaining a mutation path. */
|
|
29
30
|
list(): ExternalAgentSessionBinding[];
|
|
30
31
|
get(cli: ExternalAgentCli, sessionId: string): ExternalAgentSessionBinding | undefined;
|
|
31
|
-
record(binding: ExternalAgentSessionRecord): void
|
|
32
|
+
record(binding: ExternalAgentSessionRecord): Promise<void>;
|
|
32
33
|
private load;
|
|
34
|
+
private loadForWrite;
|
|
33
35
|
private save;
|
|
34
36
|
private withLock;
|
|
35
37
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
4
|
import { dirname, join } from "node:path";
|
|
3
5
|
import { codeShellHome, logger, normalizeCwdPath } from "@cjhyy/code-shell-core/extension";
|
|
4
6
|
const LOCK_WAIT_MS = 5_000;
|
|
@@ -9,6 +11,7 @@ export function defaultExternalAgentSessionStorePath() {
|
|
|
9
11
|
}
|
|
10
12
|
export class ExternalAgentSessionStore {
|
|
11
13
|
file;
|
|
14
|
+
pendingBindings = new Map();
|
|
12
15
|
constructor(file = defaultExternalAgentSessionStorePath()) {
|
|
13
16
|
this.file = file;
|
|
14
17
|
}
|
|
@@ -16,50 +19,40 @@ export class ExternalAgentSessionStore {
|
|
|
16
19
|
* persisted array so read-only consumers (for example room discovery) can
|
|
17
20
|
* correlate worktree sessions without gaining a mutation path. */
|
|
18
21
|
list() {
|
|
19
|
-
|
|
22
|
+
const merged = new Map(this.load().map((binding) => [bindingKey(binding), binding]));
|
|
23
|
+
for (const [key, binding] of this.pendingBindings)
|
|
24
|
+
merged.set(key, binding);
|
|
25
|
+
return [...merged.values()].map((binding) => ({ ...binding }));
|
|
20
26
|
}
|
|
21
27
|
get(cli, sessionId) {
|
|
22
28
|
if (!sessionId)
|
|
23
29
|
return undefined;
|
|
24
|
-
|
|
30
|
+
const pending = this.pendingBindings.get(bindingKey({ cli, sessionId }));
|
|
31
|
+
return pending ?? this.load().find((s) => s.cli === cli && s.sessionId === sessionId);
|
|
25
32
|
}
|
|
26
|
-
record(binding) {
|
|
33
|
+
async record(binding) {
|
|
27
34
|
if (!binding.sessionId || !binding.cwd)
|
|
28
35
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...((binding.worktreeBaseRef ?? existing?.worktreeBaseRef)
|
|
50
|
-
? { worktreeBaseRef: binding.worktreeBaseRef ?? existing?.worktreeBaseRef }
|
|
51
|
-
: {}),
|
|
52
|
-
...((binding.isolation ?? existing?.isolation)
|
|
53
|
-
? { isolation: binding.isolation ?? existing?.isolation }
|
|
54
|
-
: {}),
|
|
55
|
-
createdAt: binding.createdAt ?? existing?.createdAt ?? now,
|
|
56
|
-
lastUsedAt: now,
|
|
57
|
-
updatedAt: now,
|
|
58
|
-
};
|
|
59
|
-
const sessions = loaded.filter((s) => !(s.cli === binding.cli && s.sessionId === binding.sessionId));
|
|
60
|
-
sessions.push(next);
|
|
61
|
-
this.save(sessions);
|
|
62
|
-
});
|
|
36
|
+
const key = bindingKey(binding);
|
|
37
|
+
// Preserve the old immediate-read contract while disk persistence proceeds
|
|
38
|
+
// without blocking the event loop. This instance overlays the pending value
|
|
39
|
+
// on list/get until the atomic write finishes.
|
|
40
|
+
const optimistic = mergeBinding(binding, this.get(binding.cli, binding.sessionId));
|
|
41
|
+
this.pendingBindings.set(key, optimistic);
|
|
42
|
+
try {
|
|
43
|
+
await this.withLock(async () => {
|
|
44
|
+
const loaded = await this.loadForWrite();
|
|
45
|
+
const existing = loaded.find((s) => s.cli === binding.cli && s.sessionId === binding.sessionId);
|
|
46
|
+
const next = mergeBinding(binding, existing);
|
|
47
|
+
const sessions = loaded.filter((s) => !(s.cli === binding.cli && s.sessionId === binding.sessionId));
|
|
48
|
+
sessions.push(next);
|
|
49
|
+
await this.save(sessions);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (this.pendingBindings.get(key) === optimistic)
|
|
54
|
+
this.pendingBindings.delete(key);
|
|
55
|
+
}
|
|
63
56
|
}
|
|
64
57
|
load() {
|
|
65
58
|
if (!existsSync(this.file))
|
|
@@ -80,56 +73,111 @@ export class ExternalAgentSessionStore {
|
|
|
80
73
|
return [];
|
|
81
74
|
}
|
|
82
75
|
}
|
|
83
|
-
|
|
84
|
-
const dir = dirname(this.file);
|
|
85
|
-
if (!existsSync(dir))
|
|
86
|
-
mkdirSync(dir, { recursive: true });
|
|
87
|
-
const snapshot = { version: 2, sessions };
|
|
88
|
-
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
76
|
+
async loadForWrite() {
|
|
89
77
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
const raw = await readFile(this.file, "utf-8");
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (!parsed || !Array.isArray(parsed.sessions)) {
|
|
81
|
+
throw new Error("external agent session store has an invalid root");
|
|
82
|
+
}
|
|
83
|
+
return parsed.sessions.filter(isBinding).map(normalizeBinding);
|
|
92
84
|
}
|
|
93
85
|
catch (err) {
|
|
94
|
-
|
|
86
|
+
if (err.code === "ENOENT")
|
|
87
|
+
return [];
|
|
88
|
+
logger.warn("external_agent_session_store.write_read_failed", {
|
|
89
|
+
cat: "cc",
|
|
90
|
+
file: this.file,
|
|
91
|
+
error: err instanceof Error ? err.message : String(err),
|
|
92
|
+
});
|
|
95
93
|
throw err;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
|
-
|
|
96
|
+
async save(sessions) {
|
|
97
|
+
const dir = dirname(this.file);
|
|
98
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
99
|
+
if (process.platform !== "win32")
|
|
100
|
+
await chmod(dir, 0o700);
|
|
101
|
+
const snapshot = { version: 2, sessions };
|
|
102
|
+
const tmp = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
|
|
103
|
+
try {
|
|
104
|
+
await writeFile(tmp, JSON.stringify(snapshot, null, 2) + "\n", {
|
|
105
|
+
encoding: "utf-8",
|
|
106
|
+
mode: 0o600,
|
|
107
|
+
flag: "wx",
|
|
108
|
+
});
|
|
109
|
+
await rename(tmp, this.file);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await rm(tmp, { force: true }).catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async withLock(fn) {
|
|
99
116
|
const dir = dirname(this.file);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// otherwise block the event loop for up to LOCK_WAIT_MS under contention.
|
|
117
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
118
|
+
if (process.platform !== "win32")
|
|
119
|
+
await chmod(dir, 0o700);
|
|
104
120
|
const lockDir = `${this.file}.lock`;
|
|
105
121
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
106
122
|
while (true) {
|
|
107
123
|
try {
|
|
108
|
-
|
|
124
|
+
await mkdir(lockDir);
|
|
109
125
|
break;
|
|
110
126
|
}
|
|
111
127
|
catch (err) {
|
|
112
128
|
const code = err.code;
|
|
113
129
|
if (code !== "EEXIST")
|
|
114
130
|
throw err;
|
|
115
|
-
if (removeStaleLock(lockDir))
|
|
131
|
+
if (await removeStaleLock(lockDir))
|
|
116
132
|
continue;
|
|
117
133
|
if (Date.now() >= deadline) {
|
|
118
134
|
throw new Error(`timed out waiting for external agent session store lock: ${lockDir}`, {
|
|
119
135
|
cause: err,
|
|
120
136
|
});
|
|
121
137
|
}
|
|
122
|
-
|
|
138
|
+
await delay(LOCK_POLL_MS);
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
try {
|
|
126
|
-
return fn();
|
|
142
|
+
return await fn();
|
|
127
143
|
}
|
|
128
144
|
finally {
|
|
129
|
-
|
|
145
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
}
|
|
149
|
+
function bindingKey(binding) {
|
|
150
|
+
return `${binding.cli}\0${binding.sessionId}`;
|
|
151
|
+
}
|
|
152
|
+
function mergeBinding(binding, existing) {
|
|
153
|
+
const now = binding.lastUsedAt ?? binding.updatedAt ?? Date.now();
|
|
154
|
+
return {
|
|
155
|
+
cli: binding.cli,
|
|
156
|
+
sessionId: binding.sessionId,
|
|
157
|
+
...((binding.codeShellSessionId ?? existing?.codeShellSessionId)
|
|
158
|
+
? { codeShellSessionId: binding.codeShellSessionId ?? existing?.codeShellSessionId }
|
|
159
|
+
: {}),
|
|
160
|
+
cwd: normalizeCwdPath(binding.cwd),
|
|
161
|
+
...((binding.workspaceRoot ?? existing?.workspaceRoot)
|
|
162
|
+
? { workspaceRoot: normalizeCwdPath(binding.workspaceRoot ?? existing.workspaceRoot) }
|
|
163
|
+
: {}),
|
|
164
|
+
...((binding.worktreePath ?? existing?.worktreePath)
|
|
165
|
+
? { worktreePath: normalizeCwdPath(binding.worktreePath ?? existing.worktreePath) }
|
|
166
|
+
: {}),
|
|
167
|
+
...((binding.worktreeBranch ?? existing?.worktreeBranch)
|
|
168
|
+
? { worktreeBranch: binding.worktreeBranch ?? existing?.worktreeBranch }
|
|
169
|
+
: {}),
|
|
170
|
+
...((binding.worktreeBaseRef ?? existing?.worktreeBaseRef)
|
|
171
|
+
? { worktreeBaseRef: binding.worktreeBaseRef ?? existing?.worktreeBaseRef }
|
|
172
|
+
: {}),
|
|
173
|
+
...((binding.isolation ?? existing?.isolation)
|
|
174
|
+
? { isolation: binding.isolation ?? existing?.isolation }
|
|
175
|
+
: {}),
|
|
176
|
+
createdAt: binding.createdAt ?? existing?.createdAt ?? now,
|
|
177
|
+
lastUsedAt: now,
|
|
178
|
+
updatedAt: now,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
133
181
|
function normalizeBinding(binding) {
|
|
134
182
|
const updatedAt = finiteTimestamp(binding.updatedAt) ?? Date.now();
|
|
135
183
|
const lastUsedAt = finiteTimestamp(binding.lastUsedAt) ?? updatedAt;
|
|
@@ -147,11 +195,11 @@ function normalizeBinding(binding) {
|
|
|
147
195
|
function finiteTimestamp(value) {
|
|
148
196
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
149
197
|
}
|
|
150
|
-
function removeStaleLock(lockDir) {
|
|
198
|
+
async function removeStaleLock(lockDir) {
|
|
151
199
|
try {
|
|
152
|
-
if (Date.now() -
|
|
200
|
+
if (Date.now() - (await stat(lockDir)).mtimeMs <= LOCK_STALE_MS)
|
|
153
201
|
return false;
|
|
154
|
-
|
|
202
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
155
203
|
return true;
|
|
156
204
|
}
|
|
157
205
|
catch (err) {
|
|
@@ -160,17 +208,8 @@ function removeStaleLock(lockDir) {
|
|
|
160
208
|
return false;
|
|
161
209
|
}
|
|
162
210
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
166
|
-
Atomics.wait(view, 0, 0, ms);
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
const until = Date.now() + ms;
|
|
170
|
-
while (Date.now() < until) {
|
|
171
|
-
// fallback for runtimes where Atomics.wait is unavailable
|
|
172
|
-
}
|
|
173
|
-
}
|
|
211
|
+
function delay(ms) {
|
|
212
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
174
213
|
}
|
|
175
214
|
function isBinding(value) {
|
|
176
215
|
if (!value || typeof value !== "object")
|
|
@@ -66,15 +66,19 @@ export class ClaudeEventTranslator {
|
|
|
66
66
|
onSystem(message) {
|
|
67
67
|
if (str(message.subtype) !== "init")
|
|
68
68
|
return [];
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
this.runtimeSessionId =
|
|
69
|
+
const runtimeSessionId = str(message.session_id);
|
|
70
|
+
if (runtimeSessionId)
|
|
71
|
+
this.runtimeSessionId = runtimeSessionId;
|
|
72
72
|
if (this.sessionStarted)
|
|
73
73
|
return [];
|
|
74
74
|
this.sessionStarted = true;
|
|
75
|
+
if (!runtimeSessionId)
|
|
76
|
+
return [];
|
|
75
77
|
// promptTokens is unknown at init; the field is required, and 0 is honest
|
|
76
|
-
// here rather than a guess.
|
|
77
|
-
|
|
78
|
+
// here rather than a guess. The StreamEvent identity is CodeShell's stable
|
|
79
|
+
// business session id; Claude's id is a resume key only and must never
|
|
80
|
+
// replace the renderer's engineSessionId.
|
|
81
|
+
return [{ type: "session_started", sessionId: this.options.sessionId, promptTokens: 0 }];
|
|
78
82
|
}
|
|
79
83
|
onStreamEvent(message) {
|
|
80
84
|
const event = asRecord(message.event);
|
package/dist/index.capability.js
CHANGED
|
@@ -14,6 +14,7 @@ import { codingArtifactDetector, createCodingToolService, findCodingInstructionB
|
|
|
14
14
|
function defineTool(definition, execute, exposure) {
|
|
15
15
|
return { definition, execute, exposure };
|
|
16
16
|
}
|
|
17
|
+
const unavailableInQuickChat = (context) => context.behaviorProfile !== "quickChatRestricted";
|
|
17
18
|
export const CODING_TOOLS = [
|
|
18
19
|
// `briefTool` remains a root compatibility export, but is deliberately not
|
|
19
20
|
// model-exposed here: its Markdown return value is a tool result, not a
|
|
@@ -26,14 +27,14 @@ export const CODING_TOOLS = [
|
|
|
26
27
|
isConcurrencySafe: false,
|
|
27
28
|
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
28
29
|
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
29
|
-
}, driveAgentTool, { presetTags: ["general", "terminal-coding"] }),
|
|
30
|
+
}, driveAgentTool, { presetTags: ["general", "terminal-coding"], availability: unavailableInQuickChat }),
|
|
30
31
|
defineTool({
|
|
31
32
|
...driveAgentJobsToolDef,
|
|
32
33
|
source: "builtin",
|
|
33
34
|
permissionDefault: "allow",
|
|
34
35
|
isReadOnly: false,
|
|
35
36
|
isConcurrencySafe: false,
|
|
36
|
-
}, driveAgentJobsTool, { presetTags: ["general", "terminal-coding"] }),
|
|
37
|
+
}, driveAgentJobsTool, { presetTags: ["general", "terminal-coding"], availability: unavailableInQuickChat }),
|
|
37
38
|
defineTool({
|
|
38
39
|
...driveClaudeCodeToolDef,
|
|
39
40
|
source: "builtin",
|
|
@@ -42,7 +43,7 @@ export const CODING_TOOLS = [
|
|
|
42
43
|
isConcurrencySafe: false,
|
|
43
44
|
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
44
45
|
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
45
|
-
}, driveClaudeCodeTool, { presetTags: ["general", "terminal-coding"] }),
|
|
46
|
+
}, driveClaudeCodeTool, { presetTags: ["general", "terminal-coding"], availability: unavailableInQuickChat }),
|
|
46
47
|
defineTool({
|
|
47
48
|
...checkQuotaToolDef,
|
|
48
49
|
source: "builtin",
|
|
@@ -21,7 +21,7 @@ type Runner = (opts: {
|
|
|
21
21
|
}) => Promise<AgentRunResult>;
|
|
22
22
|
type SessionStore = {
|
|
23
23
|
get(cli: DriveCli, sessionId: string): ExternalAgentSessionBinding | undefined;
|
|
24
|
-
record(binding: ExternalAgentSessionRecord): void
|
|
24
|
+
record(binding: ExternalAgentSessionRecord): void | Promise<void>;
|
|
25
25
|
};
|
|
26
26
|
export interface DriveAgentToolOptions {
|
|
27
27
|
foregroundHandoffMs?: number;
|
|
@@ -107,6 +107,10 @@ export const driveAgentToolDef = {
|
|
|
107
107
|
required: ["prompt"],
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
|
+
/** Foreground runs are not background jobs until the handoff deadline. Keep
|
|
111
|
+
* them visible to conflict detection during that window so another session
|
|
112
|
+
* cannot launch a writer into the same workspace. */
|
|
113
|
+
const foregroundDriveLeases = new Map();
|
|
110
114
|
function isExternalRuntimeContext(ctx) {
|
|
111
115
|
return (ctx?.externalRuntime === true);
|
|
112
116
|
}
|
|
@@ -155,11 +159,20 @@ async function resolveDriveWorkspaceRoot(cwd) {
|
|
|
155
159
|
function hasRunningDriveWriter(workspaceCwd, workspaceRoot) {
|
|
156
160
|
if (backgroundJobRegistry.listRunningByCwd(workspaceCwd).some(isDriveAgentJob))
|
|
157
161
|
return true;
|
|
158
|
-
|
|
162
|
+
if (backgroundJobRegistry
|
|
159
163
|
.list()
|
|
160
164
|
.some((job) => (job.status === "running" || job.status === "cancelling") &&
|
|
161
165
|
isDriveAgentJob(job) &&
|
|
162
|
-
job.workspaceRoot === workspaceRoot)
|
|
166
|
+
job.workspaceRoot === workspaceRoot)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
for (const lease of foregroundDriveLeases.values()) {
|
|
170
|
+
if (lease.effectiveWorkspaceRoot === workspaceRoot ||
|
|
171
|
+
lease.workspaceRoot === workspaceRoot) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
163
176
|
}
|
|
164
177
|
async function prepareDriveWorktree(params) {
|
|
165
178
|
const gitRoot = await findGitRoot(params.cwd, params.signal);
|
|
@@ -401,7 +414,7 @@ function appendAttachmentPrompt(prompt, paths) {
|
|
|
401
414
|
}
|
|
402
415
|
return `${prompt}\n${lines.join("\n")}`;
|
|
403
416
|
}
|
|
404
|
-
function recordSuccessfulSession(store, cli, cwd, result, metadata, includeErroredSession = false) {
|
|
417
|
+
async function recordSuccessfulSession(store, cli, cwd, result, metadata, includeErroredSession = false) {
|
|
405
418
|
if ((!includeErroredSession && result.isError) || !result.sessionId)
|
|
406
419
|
return;
|
|
407
420
|
// When the isolation worktree directory was removed during finalization the
|
|
@@ -425,7 +438,7 @@ function recordSuccessfulSession(store, cli, cwd, result, metadata, includeError
|
|
|
425
438
|
? { worktreeBranch: metadata.worktree.session.worktreeBranch }
|
|
426
439
|
: {};
|
|
427
440
|
try {
|
|
428
|
-
store.record({
|
|
441
|
+
await store.record({
|
|
429
442
|
cli,
|
|
430
443
|
sessionId: result.sessionId,
|
|
431
444
|
...(metadata.codeShellSessionId ? { codeShellSessionId: metadata.codeShellSessionId } : {}),
|
|
@@ -445,21 +458,40 @@ function recordSuccessfulSession(store, cli, cwd, result, metadata, includeError
|
|
|
445
458
|
});
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
|
-
function
|
|
461
|
+
function duplicateCwdError(effectiveWorkspaceCwd, effectiveWorkspaceRoot, writable) {
|
|
449
462
|
if (!writable)
|
|
450
463
|
return undefined;
|
|
451
464
|
const running = backgroundJobRegistry
|
|
452
465
|
.listRunningByCwd(effectiveWorkspaceCwd)
|
|
453
466
|
.filter(isDriveAgentJob);
|
|
454
|
-
|
|
467
|
+
const foreground = [...foregroundDriveLeases.values()].filter((lease) => lease.effectiveWorkspaceRoot === effectiveWorkspaceRoot);
|
|
468
|
+
if (running.length === 0 && foreground.length === 0)
|
|
455
469
|
return undefined;
|
|
456
|
-
const jobs =
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
470
|
+
const jobs = [
|
|
471
|
+
...running.map(formatDriveJobListLine),
|
|
472
|
+
...foreground.map((lease) => `${lease.leaseId} status=foreground ${lease.label}`),
|
|
473
|
+
].join("; ");
|
|
474
|
+
return (`Error: another writable DriveAgent is already running in effective workspace ${effectiveWorkspaceRoot}. ` +
|
|
475
|
+
"Refusing to start a concurrent writer because it could overwrite the other agent's work. " +
|
|
460
476
|
`Run DriveAgentJobs(action:"list", cwd:"${effectiveWorkspaceCwd}") before dispatching parallel work for details/cancellation. ` +
|
|
461
477
|
`Running: ${jobs}`);
|
|
462
478
|
}
|
|
479
|
+
function acquireForegroundDriveLease(params) {
|
|
480
|
+
const conflict = duplicateCwdError(params.effectiveWorkspaceCwd, params.effectiveWorkspaceRoot, params.writable);
|
|
481
|
+
if (conflict)
|
|
482
|
+
return { error: conflict };
|
|
483
|
+
if (!params.writable)
|
|
484
|
+
return { release: () => undefined };
|
|
485
|
+
const leaseId = newDriveJobId();
|
|
486
|
+
const lease = { leaseId, ...params };
|
|
487
|
+
foregroundDriveLeases.set(leaseId, lease);
|
|
488
|
+
return {
|
|
489
|
+
release: () => {
|
|
490
|
+
if (foregroundDriveLeases.get(leaseId) === lease)
|
|
491
|
+
foregroundDriveLeases.delete(leaseId);
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
463
495
|
function attachDriveCompletion(params) {
|
|
464
496
|
const { jobId, sessionId, label, cli, cwd, workspaceRoot, isolation, worktree, run, sessionStore, readChangedFiles, recordExternalFileChanges, originClientMessageId, } = params;
|
|
465
497
|
void run
|
|
@@ -481,7 +513,7 @@ function attachDriveCompletion(params) {
|
|
|
481
513
|
if (lifecycle) {
|
|
482
514
|
backgroundJobRegistry.recordWorktreeLifecycle(jobId, lifecycle.action);
|
|
483
515
|
}
|
|
484
|
-
recordSuccessfulSession(sessionStore, cli, cwd, r, { codeShellSessionId: sessionId, workspaceRoot, isolation, worktree, lifecycle }, cancelling);
|
|
516
|
+
void recordSuccessfulSession(sessionStore, cli, cwd, r, { codeShellSessionId: sessionId, workspaceRoot, isolation, worktree, lifecycle }, cancelling);
|
|
485
517
|
if (changedFiles.length > 0) {
|
|
486
518
|
recordExternalFileChanges?.({
|
|
487
519
|
jobId,
|
|
@@ -574,34 +606,57 @@ function attachDriveCompletion(params) {
|
|
|
574
606
|
});
|
|
575
607
|
}
|
|
576
608
|
function trackBackgroundRun(params) {
|
|
577
|
-
const
|
|
609
|
+
const conflict = duplicateCwdError(params.effectiveWorkspaceCwd, params.effectiveWorkspaceRoot, params.writable);
|
|
610
|
+
if (conflict)
|
|
611
|
+
return { error: conflict };
|
|
578
612
|
const jobId = newDriveJobId();
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
params.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
613
|
+
let run;
|
|
614
|
+
try {
|
|
615
|
+
// Publish ownership before starting the external process. Registry
|
|
616
|
+
// listeners are synchronous and may re-enter session teardown from the
|
|
617
|
+
// start notification; starting first would leave that process orphaned.
|
|
618
|
+
backgroundJobRegistry.start(jobId, params.sessionId, params.label, {
|
|
619
|
+
kind: "drive-agent",
|
|
620
|
+
launchCwd: params.cwd,
|
|
621
|
+
effectiveWorkspaceCwd: params.effectiveWorkspaceCwd,
|
|
622
|
+
workspaceRoot: params.workspaceRoot,
|
|
623
|
+
isolation: params.isolation,
|
|
624
|
+
...(params.worktree
|
|
625
|
+
? {
|
|
626
|
+
worktreePath: params.worktree.session.worktreePath,
|
|
627
|
+
worktreeBranch: params.worktree.session.worktreeBranch,
|
|
628
|
+
worktreeBaseRef: params.worktree.session.baseRef,
|
|
629
|
+
worktreeCleanup: params.worktree.cleanup,
|
|
630
|
+
worktreeBranchPrefix: params.worktree.branchPrefix,
|
|
631
|
+
}
|
|
632
|
+
: {}),
|
|
633
|
+
cli: params.cli,
|
|
634
|
+
promptSummary: params.promptSummary,
|
|
635
|
+
originClientMessageId: params.originClientMessageId,
|
|
636
|
+
abort: async () => {
|
|
637
|
+
params.abort();
|
|
638
|
+
await run?.catch(() => undefined);
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return {
|
|
644
|
+
error: `Error: failed to register DriveAgent job: ${error instanceof Error ? error.message : String(error)}`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (backgroundJobRegistry.get(jobId)?.status !== "running") {
|
|
648
|
+
return { error: "Error: DriveAgent owner session closed before the job could start." };
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
run = params.start();
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
655
|
+
backgroundJobRegistry.finish(jobId, { status: "failed", finalText: message });
|
|
656
|
+
return { error: `Error: failed to start DriveAgent job: ${message}` };
|
|
657
|
+
}
|
|
603
658
|
attachDriveCompletion({ ...params, jobId, run });
|
|
604
|
-
return { jobId
|
|
659
|
+
return { jobId };
|
|
605
660
|
}
|
|
606
661
|
async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
607
662
|
if (!Number.isFinite(handoffMs) || handoffMs < 0) {
|
|
@@ -674,11 +729,20 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
674
729
|
: process.cwd();
|
|
675
730
|
const requestedCwd = normalizeCwdPath(rawRequestedCwd);
|
|
676
731
|
const requestedWorkspaceRoot = await resolveDriveWorkspaceRoot(requestedCwd);
|
|
732
|
+
const declaredEffectiveWorkspaceCwd = typeof args.effectiveWorkspaceCwd === "string" && args.effectiveWorkspaceCwd.trim()
|
|
733
|
+
? normalizeCwdPath(args.effectiveWorkspaceCwd)
|
|
734
|
+
: undefined;
|
|
735
|
+
const conflictCwd = declaredEffectiveWorkspaceCwd ?? requestedCwd;
|
|
736
|
+
const conflictWorkspaceRoot = conflictCwd === requestedCwd
|
|
737
|
+
? requestedWorkspaceRoot
|
|
738
|
+
: await resolveDriveWorkspaceRoot(conflictCwd);
|
|
677
739
|
const requestedIsolation = driveIsolationArg(args.isolation);
|
|
678
740
|
const hasParallelWriter = !resumeSessionId &&
|
|
679
741
|
isWritableRun &&
|
|
680
|
-
hasRunningDriveWriter(
|
|
681
|
-
|
|
742
|
+
hasRunningDriveWriter(conflictCwd, conflictWorkspaceRoot);
|
|
743
|
+
const canAutoIsolateEffectiveWorkspace = conflictWorkspaceRoot === requestedWorkspaceRoot;
|
|
744
|
+
let isolation = requestedIsolation ??
|
|
745
|
+
(hasParallelWriter && canAutoIsolateEffectiveWorkspace ? "worktree" : "none");
|
|
682
746
|
let cwd = isolation === "current" && typeof ctx?.cwd === "string" && ctx.cwd
|
|
683
747
|
? normalizeCwdPath(ctx.cwd)
|
|
684
748
|
: requestedCwd;
|
|
@@ -764,22 +828,16 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
764
828
|
workspaceRoot = managedWorktree.session.originalCwd;
|
|
765
829
|
}
|
|
766
830
|
catch (error) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
cwd = requestedCwd;
|
|
772
|
-
workspaceRoot = requestedWorkspaceRoot;
|
|
773
|
-
resumeNote =
|
|
774
|
-
`Note: parallel worktree isolation was unavailable; continuing in ${requestedCwd}. ` +
|
|
775
|
-
`${error instanceof Error ? error.message : String(error)}`;
|
|
831
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
832
|
+
return requestedIsolation === "worktree"
|
|
833
|
+
? `Error: failed to create DriveAgent isolation worktree: ${detail}`
|
|
834
|
+
: `Error: another writable DriveAgent already targets this workspace, and automatic worktree isolation failed. Refusing to run both writers in ${requestedCwd}: ${detail}`;
|
|
776
835
|
}
|
|
777
836
|
}
|
|
778
837
|
const effectiveWorkspaceCwd = managedWorktree
|
|
779
838
|
? managedWorktree.session.worktreePath
|
|
780
|
-
:
|
|
781
|
-
|
|
782
|
-
: cwd;
|
|
839
|
+
: (declaredEffectiveWorkspaceCwd ?? cwd);
|
|
840
|
+
const effectiveWorkspaceRoot = await resolveDriveWorkspaceRoot(effectiveWorkspaceCwd);
|
|
783
841
|
// Default to bypassPermissions: this tool is a fire-one-turn delegation to
|
|
784
842
|
// an external CLI with nobody watching for approvals, and there is no
|
|
785
843
|
// interactive approval loop here — so under "default" a tool that needs
|
|
@@ -825,6 +883,7 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
825
883
|
cli,
|
|
826
884
|
cwd,
|
|
827
885
|
effectiveWorkspaceCwd,
|
|
886
|
+
effectiveWorkspaceRoot,
|
|
828
887
|
workspaceRoot,
|
|
829
888
|
isolation,
|
|
830
889
|
worktree: managedWorktree,
|
|
@@ -837,16 +896,30 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
837
896
|
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
838
897
|
originClientMessageId: ctx?.originClientMessageId,
|
|
839
898
|
});
|
|
899
|
+
if ("error" in tracked) {
|
|
900
|
+
const lifecycle = safeFinalizeDriveWorktree(managedWorktree);
|
|
901
|
+
return appendLifecycleNote(tracked.error, lifecycle);
|
|
902
|
+
}
|
|
840
903
|
ctx?.runYield?.request("background_notification");
|
|
841
904
|
return [
|
|
842
905
|
resumeNote,
|
|
843
906
|
`已在后台启动 ${cliName}(jobId ${tracked.jobId})。完成后会通知你结果,无需轮询。`,
|
|
844
907
|
worktreeStartNote(managedWorktree),
|
|
845
|
-
tracked.warning,
|
|
846
908
|
]
|
|
847
909
|
.filter(Boolean)
|
|
848
910
|
.join("\n");
|
|
849
911
|
}
|
|
912
|
+
const lease = acquireForegroundDriveLease({
|
|
913
|
+
effectiveWorkspaceCwd,
|
|
914
|
+
effectiveWorkspaceRoot,
|
|
915
|
+
workspaceRoot,
|
|
916
|
+
label,
|
|
917
|
+
writable: isWritableRun,
|
|
918
|
+
});
|
|
919
|
+
if ("error" in lease) {
|
|
920
|
+
const lifecycle = safeFinalizeDriveWorktree(managedWorktree);
|
|
921
|
+
return appendLifecycleNote(lease.error, lifecycle);
|
|
922
|
+
}
|
|
850
923
|
const foregroundAbort = makeAbortController(callerSignal, true);
|
|
851
924
|
const run = startRun(runner, { ...runOptsBase, signal: foregroundAbort.signal });
|
|
852
925
|
let result;
|
|
@@ -855,15 +928,21 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
855
928
|
}
|
|
856
929
|
catch (error) {
|
|
857
930
|
const lifecycle = safeFinalizeDriveWorktree(managedWorktree);
|
|
931
|
+
lease.release();
|
|
858
932
|
return appendLifecycleNote(`${cliName} 运行出错:${error instanceof Error ? error.message : String(error)}`, lifecycle);
|
|
859
933
|
}
|
|
860
934
|
if (result.kind === "handoff" && isValidSessionId(ctx?.sessionId)) {
|
|
935
|
+
// Registration below is synchronous. Release the foreground lease and
|
|
936
|
+
// replace it with the background registry entry in the same event-loop
|
|
937
|
+
// turn, so another dispatch cannot slip into a gap.
|
|
938
|
+
lease.release();
|
|
861
939
|
const tracked = trackBackgroundRun({
|
|
862
940
|
sessionId: ctx.sessionId,
|
|
863
941
|
label,
|
|
864
942
|
cli,
|
|
865
943
|
cwd,
|
|
866
944
|
effectiveWorkspaceCwd,
|
|
945
|
+
effectiveWorkspaceRoot,
|
|
867
946
|
workspaceRoot,
|
|
868
947
|
isolation,
|
|
869
948
|
worktree: managedWorktree,
|
|
@@ -876,30 +955,39 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
876
955
|
recordExternalFileChanges: ctx?.recordExternalFileChanges,
|
|
877
956
|
originClientMessageId: ctx?.originClientMessageId,
|
|
878
957
|
});
|
|
958
|
+
if ("error" in tracked) {
|
|
959
|
+
foregroundAbort.abort();
|
|
960
|
+
const lifecycle = safeFinalizeDriveWorktree(managedWorktree);
|
|
961
|
+
return appendLifecycleNote(tracked.error, lifecycle);
|
|
962
|
+
}
|
|
879
963
|
ctx?.runYield?.request("background_notification");
|
|
880
964
|
return [
|
|
881
965
|
resumeNote,
|
|
882
966
|
`${cliName} foreground run exceeded ${foregroundHandoffMs}ms; moved it to background (jobId ${tracked.jobId}). Completion will notify this session, so do not poll.`,
|
|
883
967
|
worktreeStartNote(managedWorktree),
|
|
884
|
-
tracked.warning,
|
|
885
968
|
]
|
|
886
969
|
.filter(Boolean)
|
|
887
970
|
.join("\n");
|
|
888
971
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
972
|
+
try {
|
|
973
|
+
const r = result.kind === "completed" ? result.result : await run;
|
|
974
|
+
const lifecycle = safeFinalizeDriveWorktree(managedWorktree);
|
|
975
|
+
await recordSuccessfulSession(sessionStore, cli, cwd, r, {
|
|
976
|
+
codeShellSessionId: ctx?.sessionId,
|
|
977
|
+
workspaceRoot,
|
|
978
|
+
isolation,
|
|
979
|
+
worktree: managedWorktree,
|
|
980
|
+
lifecycle,
|
|
981
|
+
});
|
|
982
|
+
const prefix = resumeNote ? `${resumeNote}\n` : "";
|
|
983
|
+
const finalText = appendLifecycleNote(r.finalText, lifecycle);
|
|
984
|
+
if (r.isError)
|
|
985
|
+
return `${prefix}${cliName} 运行出错(session ${r.sessionId}):\n${finalText}`;
|
|
986
|
+
return `${prefix}${cliName} 完成(session ${r.sessionId}):\n${finalText}`;
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
lease.release();
|
|
990
|
+
}
|
|
903
991
|
};
|
|
904
992
|
}
|
|
905
993
|
export const driveAgentTool = makeDriveAgentTool();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-capability-coding",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
4
4
|
"description": "Coding capability pack for the generic code-shell agent core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "bun run clean && tsc -p tsconfig.json && bun run copy-assets",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
38
|
"copy-assets": "node ../../scripts/copy-assets.mjs dist/prompt src/prompt/*.md && node ../../scripts/copy-assets.mjs dist/tools/apply-patch src/tools/apply-patch/NOTICE.md src/tools/apply-patch/LICENSE-codex",
|
|
38
39
|
"dev": "bun run copy-assets && tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
39
40
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@cjhyy/code-shell-core": "0.8.
|
|
43
|
+
"@cjhyy/code-shell-core": "0.8.10"
|
|
43
44
|
},
|
|
44
45
|
"engines": {
|
|
45
46
|
"node": ">=20.10"
|
|
@@ -57,4 +58,4 @@
|
|
|
57
58
|
"publishConfig": {
|
|
58
59
|
"access": "public"
|
|
59
60
|
}
|
|
60
|
-
}
|
|
61
|
+
}
|