@love-moon/ai-sdk 0.16.0 → 0.17.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d088b31: Claude tasks keep one Claude process per session, with its input left open
|
|
8
|
+
between turns, instead of starting a new one-shot process for every turn.
|
|
9
|
+
Background subagents now run to completion: before, claude killed them 10
|
|
10
|
+
minutes after the main agent's turn ended, so long multi-agent tasks went quiet
|
|
11
|
+
until the user sent another message. When they finish, Claude's follow-up reply
|
|
12
|
+
is posted to the chat without waiting for a user message. Subagent narration is
|
|
13
|
+
no longer posted as a task reply.
|
|
14
|
+
- 79ca552: Claude no longer switches to `acceptEdits` mode when the daemon runs as root.
|
|
15
|
+
That fallback looked like it worked, but in a headless task it silently refused
|
|
16
|
+
every shell command (ssh, rsync, ...) and every read outside the project. Claude
|
|
17
|
+
now keeps `bypassPermissions` everywhere. As root without `IS_SANDBOX=1`, the
|
|
18
|
+
chat shows a warning right after "session started" telling you to add
|
|
19
|
+
`envs: { IS_SANDBOX: "1" }` to the conductor config (or run the daemon as a
|
|
20
|
+
regular user). Terminal tasks print the same hint. An `IS_SANDBOX` set in config
|
|
21
|
+
`envs` now also reaches terminal (PTY) claude tasks. `conductor config` always
|
|
22
|
+
writes `--dangerously-skip-permissions` for claude.
|
|
23
|
+
- ccbfeeb: Kimi Code tasks no longer kill background subagents after 12 minutes. `kimi -p`
|
|
24
|
+
stays silent while it waits for a task it launched with `run_in_background`,
|
|
25
|
+
and the session's idle deadline used to treat that silence as a stuck turn and
|
|
26
|
+
terminate kimi together with the task. Once a turn launches a background task,
|
|
27
|
+
the deadline now waits for kimi to exit on its own.
|
|
28
|
+
|
|
3
29
|
## 0.16.0
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
|
|
|
2
2
|
export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
|
|
3
3
|
export { summarizeTurnUsage } from "./turn-usage.js";
|
|
4
4
|
export { createAiSession, RemoteAiSession } from "./client.js";
|
|
5
|
-
export { isClaudeRootPermissionRestricted,
|
|
5
|
+
export { isClaudeRootPermissionRestricted, buildClaudeRootSandboxNotice, claudeCommandNeedsRootSandbox, resolveClaudePermissionPolicy } from "./providers/claude-agent-sdk-session.js";
|
|
6
6
|
export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
|
|
7
7
|
export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia } from "./media-input.js";
|
|
8
8
|
export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createAiSession, RemoteAiSession } from "./client.js";
|
|
2
2
|
export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
|
|
3
3
|
export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
|
|
4
|
-
export { isClaudeRootPermissionRestricted,
|
|
4
|
+
export { isClaudeRootPermissionRestricted, buildClaudeRootSandboxNotice, claudeCommandNeedsRootSandbox, resolveClaudePermissionPolicy, } from "./providers/claude-agent-sdk-session.js";
|
|
5
5
|
export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
|
|
6
6
|
export { summarizeTurnUsage } from "./turn-usage.js";
|
|
7
7
|
export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia, } from "./media-input.js";
|
|
@@ -2,10 +2,11 @@ export function isClaudeRootPermissionRestricted(env?: NodeJS.ProcessEnv): boole
|
|
|
2
2
|
export function resolveClaudePermissionPolicy(options?: {}, env?: NodeJS.ProcessEnv): {
|
|
3
3
|
permissionMode: string;
|
|
4
4
|
allowDangerouslySkipPermissions: boolean;
|
|
5
|
-
|
|
5
|
+
rootSandboxRequired: boolean;
|
|
6
6
|
invalidMode: any;
|
|
7
7
|
};
|
|
8
|
-
export function
|
|
8
|
+
export function claudeCommandNeedsRootSandbox(commandLine: any, env?: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
export function buildClaudeRootSandboxNotice({ configFile }?: {}): string;
|
|
9
10
|
export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
10
11
|
static capabilities: Readonly<{
|
|
11
12
|
goal: true;
|
|
@@ -45,7 +46,10 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
45
46
|
lastReplyTarget: string;
|
|
46
47
|
manualResumeReady: boolean;
|
|
47
48
|
currentTurn: {
|
|
48
|
-
abortController:
|
|
49
|
+
abortController: null;
|
|
50
|
+
liveQuery: null;
|
|
51
|
+
onProgress: null;
|
|
52
|
+
settle: null;
|
|
49
53
|
suppressReply: boolean;
|
|
50
54
|
compactMetadata: null;
|
|
51
55
|
compactError: string;
|
|
@@ -56,7 +60,27 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
56
60
|
query: null;
|
|
57
61
|
resultMessage: null;
|
|
58
62
|
terminalWorkingStatusEmitted: boolean;
|
|
63
|
+
usageBase: null;
|
|
59
64
|
usageByMessageId: Map<any, any>;
|
|
65
|
+
uuid: string;
|
|
66
|
+
} | null;
|
|
67
|
+
liveQuery: {
|
|
68
|
+
abortController: AbortController;
|
|
69
|
+
backgroundTurn: null;
|
|
70
|
+
ended: boolean;
|
|
71
|
+
input: {
|
|
72
|
+
push(message: any): boolean;
|
|
73
|
+
close: () => void;
|
|
74
|
+
[Symbol.asyncIterator](): {
|
|
75
|
+
next: () => Promise<any>;
|
|
76
|
+
return: () => Promise<{
|
|
77
|
+
value: undefined;
|
|
78
|
+
done: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
query: null;
|
|
83
|
+
usageBase: null;
|
|
60
84
|
} | null;
|
|
61
85
|
lastResult: any;
|
|
62
86
|
rateLimitInfo: any;
|
|
@@ -69,9 +93,10 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
69
93
|
permissionPolicy: {
|
|
70
94
|
permissionMode: string;
|
|
71
95
|
allowDangerouslySkipPermissions: boolean;
|
|
72
|
-
|
|
96
|
+
rootSandboxRequired: boolean;
|
|
73
97
|
invalidMode: any;
|
|
74
98
|
};
|
|
99
|
+
notices: string[];
|
|
75
100
|
writeLog(message: any): void;
|
|
76
101
|
trace(message: any): void;
|
|
77
102
|
get threadId(): any;
|
|
@@ -103,6 +128,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
103
128
|
image: "native";
|
|
104
129
|
}>;
|
|
105
130
|
};
|
|
131
|
+
notices: string[];
|
|
106
132
|
};
|
|
107
133
|
getSessionInfo(): {
|
|
108
134
|
backend: string;
|
|
@@ -165,6 +191,65 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
165
191
|
onProgress: any;
|
|
166
192
|
}): Promise<void>;
|
|
167
193
|
interruptCurrentTurn(): Promise<boolean>;
|
|
194
|
+
createTurnState({ suppressReply, onProgress }?: {
|
|
195
|
+
suppressReply?: boolean | undefined;
|
|
196
|
+
onProgress?: null | undefined;
|
|
197
|
+
}): {
|
|
198
|
+
abortController: null;
|
|
199
|
+
liveQuery: null;
|
|
200
|
+
onProgress: null;
|
|
201
|
+
settle: null;
|
|
202
|
+
suppressReply: boolean;
|
|
203
|
+
compactMetadata: null;
|
|
204
|
+
compactError: string;
|
|
205
|
+
conversationReset: string;
|
|
206
|
+
emittedAssistantMessage: boolean;
|
|
207
|
+
fullText: string;
|
|
208
|
+
items: never[];
|
|
209
|
+
query: null;
|
|
210
|
+
resultMessage: null;
|
|
211
|
+
terminalWorkingStatusEmitted: boolean;
|
|
212
|
+
usageBase: null;
|
|
213
|
+
usageByMessageId: Map<any, any>;
|
|
214
|
+
uuid: string;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* One claude process per session, fed through a prompt stream that stays
|
|
218
|
+
* open between turns. A one-shot string prompt closes claude's input at once,
|
|
219
|
+
* and claude then kills background subagents 10 minutes after the main
|
|
220
|
+
* agent's turn ends (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS). Kept open, the
|
|
221
|
+
* subagents run to completion and claude starts its own follow-up turn on
|
|
222
|
+
* their notifications.
|
|
223
|
+
*/
|
|
224
|
+
ensureLiveQuery(sdkModule: any): {
|
|
225
|
+
abortController: AbortController;
|
|
226
|
+
backgroundTurn: null;
|
|
227
|
+
ended: boolean;
|
|
228
|
+
input: {
|
|
229
|
+
push(message: any): boolean;
|
|
230
|
+
close: () => void;
|
|
231
|
+
[Symbol.asyncIterator](): {
|
|
232
|
+
next: () => Promise<any>;
|
|
233
|
+
return: () => Promise<{
|
|
234
|
+
value: undefined;
|
|
235
|
+
done: boolean;
|
|
236
|
+
}>;
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
query: null;
|
|
240
|
+
usageBase: null;
|
|
241
|
+
};
|
|
242
|
+
closeLiveQuery(liveQuery: any): void;
|
|
243
|
+
settleTurn(turn: any, error: any, resultMessage?: null): void;
|
|
244
|
+
pumpLiveQuery(liveQuery: any): Promise<void>;
|
|
245
|
+
/**
|
|
246
|
+
* Routes the long-lived query's output. A result ends the pending turn when it
|
|
247
|
+
* echoes that turn's user message uuid (or echoes none while no follow-up turn
|
|
248
|
+
* of claude's own is running); everything else is a background turn, e.g.
|
|
249
|
+
* claude answering a finished subagent's notification between user turns.
|
|
250
|
+
*/
|
|
251
|
+
dispatchLiveMessage(liveQuery: any, message: any): Promise<void>;
|
|
252
|
+
finishBackgroundTurn(backgroundTurn: any, resultMessage: any, failure?: null): Promise<void>;
|
|
168
253
|
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
|
|
169
254
|
useInitialImages?: boolean | undefined;
|
|
170
255
|
onProgress?: null | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { EventEmitter } from "node:events";
|
|
2
3
|
import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in-backends.js";
|
|
3
4
|
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
@@ -7,24 +8,89 @@ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus
|
|
|
7
8
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
8
9
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
9
10
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
11
|
+
// How long an interrupted turn of the long-lived query may take to report its
|
|
12
|
+
// result before the process is torn down instead.
|
|
13
|
+
const INTERRUPT_GRACE_MS = 10 * 1000;
|
|
10
14
|
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
11
15
|
const PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"]);
|
|
12
16
|
const DEFAULT_PERMISSION_MODE = "bypassPermissions";
|
|
13
|
-
// Auto ("accept edits") mode: the strongest mode claude allows as root.
|
|
14
|
-
const ROOT_PERMISSION_MODE = "acceptEdits";
|
|
15
17
|
function waitForever() {
|
|
16
18
|
return new Promise(() => { });
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
function buildClaudeUserMessage(promptText, media, uuid) {
|
|
21
|
+
return {
|
|
20
22
|
type: "user",
|
|
21
23
|
message: {
|
|
22
24
|
role: "user",
|
|
23
|
-
content: buildClaudeContent(promptText, media),
|
|
25
|
+
content: media.length ? buildClaudeContent(promptText, media) : promptText,
|
|
24
26
|
},
|
|
25
27
|
parent_tool_use_id: null,
|
|
28
|
+
...(uuid ? { uuid } : {}),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
31
|
+
async function* buildClaudeInput(promptText, media) {
|
|
32
|
+
yield buildClaudeUserMessage(promptText, media);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The prompt stream of a long-lived query. It stays open between turns: claude
|
|
36
|
+
* only winds a print-mode process down (killing background subagents after its
|
|
37
|
+
* wait ceiling) once its input is closed.
|
|
38
|
+
*/
|
|
39
|
+
function createInputQueue() {
|
|
40
|
+
const buffer = [];
|
|
41
|
+
let waiter = null;
|
|
42
|
+
let closed = false;
|
|
43
|
+
const finish = () => {
|
|
44
|
+
closed = true;
|
|
45
|
+
if (waiter) {
|
|
46
|
+
const resolve = waiter;
|
|
47
|
+
waiter = null;
|
|
48
|
+
resolve({ value: undefined, done: true });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
push(message) {
|
|
53
|
+
if (closed) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (waiter) {
|
|
57
|
+
const resolve = waiter;
|
|
58
|
+
waiter = null;
|
|
59
|
+
resolve({ value: message, done: false });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
buffer.push(message);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
close: finish,
|
|
67
|
+
[Symbol.asyncIterator]() {
|
|
68
|
+
return {
|
|
69
|
+
next: () => {
|
|
70
|
+
if (buffer.length) {
|
|
71
|
+
return Promise.resolve({ value: buffer.shift(), done: false });
|
|
72
|
+
}
|
|
73
|
+
if (closed) {
|
|
74
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
waiter = resolve;
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
return: () => {
|
|
81
|
+
finish();
|
|
82
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function resultUserMessageUuids(resultMessage) {
|
|
89
|
+
if (Array.isArray(resultMessage?.user_message_uuids)) {
|
|
90
|
+
return resultMessage.user_message_uuids;
|
|
91
|
+
}
|
|
92
|
+
return resultMessage?.user_message_uuid ? [resultMessage.user_message_uuid] : [];
|
|
93
|
+
}
|
|
28
94
|
function createTurnError(message, extras = {}) {
|
|
29
95
|
const error = new Error(message);
|
|
30
96
|
for (const [key, value] of Object.entries(extras)) {
|
|
@@ -52,11 +118,27 @@ function sumStreamedUsage(usageByMessageId, base = null) {
|
|
|
52
118
|
return total;
|
|
53
119
|
}
|
|
54
120
|
/**
|
|
55
|
-
* A finished turn's usage.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
121
|
+
* A finished turn's usage. `usage` covers only the main loop's last segment, while
|
|
122
|
+
* `modelUsage` accumulates every API call the query's process made (all segments,
|
|
123
|
+
* subagents and earlier turns of a long-lived query), so prefer its sum minus `base`,
|
|
124
|
+
* the total at the end of the previous turn.
|
|
58
125
|
*/
|
|
59
|
-
function resultUsage(resultMessage) {
|
|
126
|
+
function resultUsage(resultMessage, base = null) {
|
|
127
|
+
const total = modelUsageTotal(resultMessage);
|
|
128
|
+
if (!total) {
|
|
129
|
+
return resultMessage.usage ? { ...resultMessage.usage } : null;
|
|
130
|
+
}
|
|
131
|
+
// /clear resets the running total; a total below its base starts over.
|
|
132
|
+
if (!base || Object.keys(base).some((key) => !(total[key] >= base[key]))) {
|
|
133
|
+
return total;
|
|
134
|
+
}
|
|
135
|
+
const delta = {};
|
|
136
|
+
for (const [key, value] of Object.entries(total)) {
|
|
137
|
+
delta[key] = value - base[key];
|
|
138
|
+
}
|
|
139
|
+
return delta;
|
|
140
|
+
}
|
|
141
|
+
function modelUsageTotal(resultMessage) {
|
|
60
142
|
const total = {};
|
|
61
143
|
for (const entry of Object.values(resultMessage.modelUsage || {})) {
|
|
62
144
|
for (const [from, to] of [
|
|
@@ -71,10 +153,7 @@ function resultUsage(resultMessage) {
|
|
|
71
153
|
}
|
|
72
154
|
}
|
|
73
155
|
}
|
|
74
|
-
|
|
75
|
-
return total;
|
|
76
|
-
}
|
|
77
|
-
return resultMessage.usage ? { ...resultMessage.usage } : null;
|
|
156
|
+
return Object.keys(total).length ? total : null;
|
|
78
157
|
}
|
|
79
158
|
function normalizeClaudeBackend(backend) {
|
|
80
159
|
const normalized = String(backend || "").trim().toLowerCase();
|
|
@@ -110,71 +189,76 @@ function normalizePermissionMode(value) {
|
|
|
110
189
|
// Two different env idioms in that one line, and both are load-bearing:
|
|
111
190
|
// IS_SANDBOX is a *strict* `=== "1"` compare, while CLAUDE_CODE_BUBBLEWRAP goes
|
|
112
191
|
// through claude's loose truthy parser (`1|true|yes|on`, trimmed+lowercased).
|
|
113
|
-
// Do not "helpfully" accept IS_SANDBOX=true here —
|
|
114
|
-
// the
|
|
115
|
-
//
|
|
192
|
+
// Do not "helpfully" accept IS_SANDBOX=true here — claude would still refuse,
|
|
193
|
+
// and the user would get no hint telling them why.
|
|
194
|
+
//
|
|
195
|
+
// Values are compared the way claude will see them: spawn stringifies env, so
|
|
196
|
+
// YAML `IS_SANDBOX: 1` (a number from config `envs:`) reaches claude as "1"
|
|
197
|
+
// and passes its gate. `true` becomes "true" and still does not.
|
|
116
198
|
export function isClaudeRootPermissionRestricted(env = process.env) {
|
|
117
199
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
118
200
|
return false;
|
|
119
201
|
}
|
|
120
|
-
if (env?.IS_SANDBOX === "1") {
|
|
202
|
+
if (String(env?.IS_SANDBOX ?? "") === "1") {
|
|
121
203
|
return false;
|
|
122
204
|
}
|
|
123
205
|
const bubblewrap = String(env?.CLAUDE_CODE_BUBBLEWRAP ?? "").trim().toLowerCase();
|
|
124
206
|
return !["1", "true", "yes", "on"].includes(bubblewrap);
|
|
125
207
|
}
|
|
126
|
-
//
|
|
127
|
-
//
|
|
208
|
+
// The configured mode is used as-is, including on root. We used to downgrade
|
|
209
|
+
// bypassPermissions -> acceptEdits for root, but a headless session has no one
|
|
210
|
+
// to approve prompts, so acceptEdits silently denied every Bash command and
|
|
211
|
+
// every read outside cwd: the task "ran" and could do nothing. Instead we keep
|
|
212
|
+
// the mode and flag `rootSandboxRequired` so the caller can tell the user to
|
|
213
|
+
// opt in with IS_SANDBOX=1 (or run as a regular user).
|
|
128
214
|
export function resolveClaudePermissionPolicy(options = {}, env = process.env) {
|
|
129
|
-
const
|
|
215
|
+
const permissionMode = normalizePermissionMode(options.permissionMode);
|
|
130
216
|
// A configured value we don't recognize still falls back to the default (a
|
|
131
217
|
// typo must never take the session down), but the caller reports it so the
|
|
132
218
|
// user finds out their config line is doing nothing.
|
|
133
219
|
const rawMode = typeof options.permissionMode === "string" ? options.permissionMode.trim() : "";
|
|
134
220
|
const invalidMode = rawMode && !PERMISSION_MODES.has(rawMode) ? rawMode : "";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
permissionMode: requested,
|
|
138
|
-
allowDangerouslySkipPermissions: requested === DEFAULT_PERMISSION_MODE || options.allowDangerouslySkipPermissions === true,
|
|
139
|
-
downgradedFrom: "",
|
|
140
|
-
invalidMode,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const permissionMode = requested === DEFAULT_PERMISSION_MODE ? ROOT_PERMISSION_MODE : requested;
|
|
221
|
+
const rootRestricted = isClaudeRootPermissionRestricted(env);
|
|
222
|
+
const bypass = permissionMode === DEFAULT_PERMISSION_MODE;
|
|
144
223
|
return {
|
|
145
224
|
permissionMode,
|
|
146
|
-
|
|
147
|
-
|
|
225
|
+
// A non-bypass mode the user picked on purpose never needs the escape
|
|
226
|
+
// flag, and as root it would only trip claude's gate.
|
|
227
|
+
allowDangerouslySkipPermissions: bypass || (options.allowDangerouslySkipPermissions === true && !rootRestricted),
|
|
228
|
+
rootSandboxRequired: bypass && rootRestricted,
|
|
148
229
|
invalidMode,
|
|
149
230
|
};
|
|
150
231
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
|
|
232
|
+
// True when a configured claude command line would hit claude's root gate:
|
|
233
|
+
// it asks for bypass (the flag, or `--permission-mode bypassPermissions`) and
|
|
234
|
+
// the process env doesn't opt out.
|
|
235
|
+
export function claudeCommandNeedsRootSandbox(commandLine, env = process.env) {
|
|
156
236
|
const command = typeof commandLine === "string" ? commandLine : "";
|
|
157
|
-
if (!command.trim()
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.trim()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
237
|
+
if (!command.trim()) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const bypass = /(^|\s)--dangerously-skip-permissions(?=\s|$)/.test(command)
|
|
241
|
+
|| extractLongFlagFromCommandLine(command, "permission-mode") === DEFAULT_PERMISSION_MODE;
|
|
242
|
+
return bypass && isClaudeRootPermissionRestricted(env);
|
|
243
|
+
}
|
|
244
|
+
// User-facing hint shown in the task chat (and the PTY terminal) when claude
|
|
245
|
+
// is about to be started as root in bypass mode without IS_SANDBOX=1.
|
|
246
|
+
export function buildClaudeRootSandboxNotice({ configFile } = {}) {
|
|
247
|
+
const configPath = typeof configFile === "string" && configFile.trim()
|
|
248
|
+
? configFile.trim()
|
|
249
|
+
: "~/.conductor/config.yaml";
|
|
250
|
+
return [
|
|
251
|
+
"⚠️ 当前 conductor 以 root 用户运行 claude。Claude Code 禁止 root 使用 bypassPermissions(--dangerously-skip-permissions),本会话的请求会直接失败。",
|
|
252
|
+
"",
|
|
253
|
+
`如果这台机器是隔离环境(容器 / 虚机),请在 ${configPath} 中加入下面的配置,然后重启 daemon:`,
|
|
254
|
+
"",
|
|
255
|
+
"```yaml",
|
|
256
|
+
"envs:",
|
|
257
|
+
" IS_SANDBOX: \"1\"",
|
|
258
|
+
"```",
|
|
259
|
+
"",
|
|
260
|
+
"注意值必须是 1(写成 1 或 \"1\" 都可以),写成 true / yes 不生效。设置后 claude 会以 root 身份直接执行命令、不再请求确认;如果不是隔离环境,建议改用普通用户运行 daemon。",
|
|
261
|
+
].join("\n");
|
|
178
262
|
}
|
|
179
263
|
function normalizeText(value) {
|
|
180
264
|
return typeof value === "string" ? value : "";
|
|
@@ -315,6 +399,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
315
399
|
this.lastReplyTarget = "";
|
|
316
400
|
this.manualResumeReady = Boolean(this.sessionId);
|
|
317
401
|
this.currentTurn = null;
|
|
402
|
+
this.liveQuery = null;
|
|
318
403
|
this.lastResult = null;
|
|
319
404
|
this.rateLimitInfo = null;
|
|
320
405
|
this.currentTurnStatus = null;
|
|
@@ -340,9 +425,15 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
340
425
|
+ `using ${this.permissionPolicy.permissionMode} instead `
|
|
341
426
|
+ `(valid: ${[...PERMISSION_MODES].join(", ")})`);
|
|
342
427
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
428
|
+
// Shown to the user by fire as a chat message (see getSnapshot().notices):
|
|
429
|
+
// claude will refuse to start, and a daemon log line alone is invisible.
|
|
430
|
+
this.notices = [];
|
|
431
|
+
if (this.permissionPolicy.rootSandboxRequired) {
|
|
432
|
+
this.trace("WARN running as root without IS_SANDBOX=1; claude will refuse bypassPermissions "
|
|
433
|
+
+ "(set envs.IS_SANDBOX: \"1\" in the conductor config, or run as a regular user)");
|
|
434
|
+
this.notices.push(buildClaudeRootSandboxNotice({
|
|
435
|
+
configFile: options.configFile || this.env.CONDUCTOR_CONFIG || process.env.CONDUCTOR_CONFIG,
|
|
436
|
+
}));
|
|
346
437
|
}
|
|
347
438
|
}
|
|
348
439
|
writeLog(message) {
|
|
@@ -381,6 +472,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
381
472
|
: null,
|
|
382
473
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
383
474
|
capabilities: this.getCapabilities(),
|
|
475
|
+
notices: [...this.notices],
|
|
384
476
|
};
|
|
385
477
|
}
|
|
386
478
|
getSessionInfo() {
|
|
@@ -839,7 +931,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
839
931
|
noteToolStarted(currentTurn, block.id, block.name, block.input);
|
|
840
932
|
}
|
|
841
933
|
}
|
|
842
|
-
|
|
934
|
+
// Subagent narration (parent_tool_use_id set) is not a reply to the user.
|
|
935
|
+
const text = message.parent_tool_use_id ? "" : extractAssistantText(message.message);
|
|
843
936
|
if (!text) {
|
|
844
937
|
return;
|
|
845
938
|
}
|
|
@@ -893,6 +986,26 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
893
986
|
if (!currentTurn) {
|
|
894
987
|
return false;
|
|
895
988
|
}
|
|
989
|
+
const { liveQuery } = currentTurn;
|
|
990
|
+
if (liveQuery) {
|
|
991
|
+
// The interrupted turn reports its own result; tear the process down only
|
|
992
|
+
// if it does not, so a stuck CLI cannot hold the turn open.
|
|
993
|
+
try {
|
|
994
|
+
await liveQuery.query.interrupt?.();
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
// best effort
|
|
998
|
+
}
|
|
999
|
+
if (this.currentTurn === currentTurn) {
|
|
1000
|
+
const timer = setTimeout(() => {
|
|
1001
|
+
if (this.currentTurn === currentTurn) {
|
|
1002
|
+
this.closeLiveQuery(liveQuery);
|
|
1003
|
+
}
|
|
1004
|
+
}, INTERRUPT_GRACE_MS);
|
|
1005
|
+
timer.unref?.();
|
|
1006
|
+
}
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
896
1009
|
try {
|
|
897
1010
|
await currentTurn.query?.interrupt?.();
|
|
898
1011
|
}
|
|
@@ -913,6 +1026,151 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
913
1026
|
}
|
|
914
1027
|
return true;
|
|
915
1028
|
}
|
|
1029
|
+
createTurnState({ suppressReply = false, onProgress = null } = {}) {
|
|
1030
|
+
return {
|
|
1031
|
+
abortController: null,
|
|
1032
|
+
liveQuery: null,
|
|
1033
|
+
onProgress,
|
|
1034
|
+
settle: null,
|
|
1035
|
+
suppressReply,
|
|
1036
|
+
compactMetadata: null,
|
|
1037
|
+
compactError: "",
|
|
1038
|
+
conversationReset: "",
|
|
1039
|
+
emittedAssistantMessage: false,
|
|
1040
|
+
fullText: "",
|
|
1041
|
+
items: [],
|
|
1042
|
+
query: null,
|
|
1043
|
+
resultMessage: null,
|
|
1044
|
+
terminalWorkingStatusEmitted: false,
|
|
1045
|
+
usageBase: null,
|
|
1046
|
+
usageByMessageId: new Map(),
|
|
1047
|
+
uuid: "",
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* One claude process per session, fed through a prompt stream that stays
|
|
1052
|
+
* open between turns. A one-shot string prompt closes claude's input at once,
|
|
1053
|
+
* and claude then kills background subagents 10 minutes after the main
|
|
1054
|
+
* agent's turn ends (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS). Kept open, the
|
|
1055
|
+
* subagents run to completion and claude starts its own follow-up turn on
|
|
1056
|
+
* their notifications.
|
|
1057
|
+
*/
|
|
1058
|
+
ensureLiveQuery(sdkModule) {
|
|
1059
|
+
if (this.liveQuery && !this.liveQuery.ended) {
|
|
1060
|
+
return this.liveQuery;
|
|
1061
|
+
}
|
|
1062
|
+
const abortController = new AbortController();
|
|
1063
|
+
const liveQuery = {
|
|
1064
|
+
abortController,
|
|
1065
|
+
backgroundTurn: null,
|
|
1066
|
+
ended: false,
|
|
1067
|
+
input: createInputQueue(),
|
|
1068
|
+
query: null,
|
|
1069
|
+
usageBase: null,
|
|
1070
|
+
};
|
|
1071
|
+
liveQuery.query = sdkModule.query({
|
|
1072
|
+
prompt: liveQuery.input,
|
|
1073
|
+
options: this.buildSdkOptions(abortController),
|
|
1074
|
+
});
|
|
1075
|
+
this.liveQuery = liveQuery;
|
|
1076
|
+
void this.pumpLiveQuery(liveQuery);
|
|
1077
|
+
return liveQuery;
|
|
1078
|
+
}
|
|
1079
|
+
closeLiveQuery(liveQuery) {
|
|
1080
|
+
liveQuery.ended = true;
|
|
1081
|
+
if (this.liveQuery === liveQuery) {
|
|
1082
|
+
this.liveQuery = null;
|
|
1083
|
+
}
|
|
1084
|
+
liveQuery.input.close();
|
|
1085
|
+
try {
|
|
1086
|
+
liveQuery.abortController.abort();
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
// best effort
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
liveQuery.query?.close?.();
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
// best effort
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
settleTurn(turn, error, resultMessage = null) {
|
|
1099
|
+
const settle = turn.settle;
|
|
1100
|
+
turn.settle = null;
|
|
1101
|
+
settle?.(error, resultMessage);
|
|
1102
|
+
}
|
|
1103
|
+
async pumpLiveQuery(liveQuery) {
|
|
1104
|
+
let failure = null;
|
|
1105
|
+
try {
|
|
1106
|
+
for await (const message of liveQuery.query) {
|
|
1107
|
+
await this.dispatchLiveMessage(liveQuery, message);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
failure = error;
|
|
1112
|
+
}
|
|
1113
|
+
this.closeLiveQuery(liveQuery);
|
|
1114
|
+
const turn = this.currentTurn;
|
|
1115
|
+
if (turn?.liveQuery === liveQuery && turn.settle) {
|
|
1116
|
+
this.settleTurn(turn, failure, turn.resultMessage);
|
|
1117
|
+
}
|
|
1118
|
+
else if (failure && !this.closeRequested) {
|
|
1119
|
+
this.trace(`claude process ended between turns: ${failure?.message || failure}`);
|
|
1120
|
+
}
|
|
1121
|
+
const backgroundTurn = liveQuery.backgroundTurn;
|
|
1122
|
+
liveQuery.backgroundTurn = null;
|
|
1123
|
+
if (backgroundTurn) {
|
|
1124
|
+
await this.finishBackgroundTurn(backgroundTurn, null, failure);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Routes the long-lived query's output. A result ends the pending turn when it
|
|
1129
|
+
* echoes that turn's user message uuid (or echoes none while no follow-up turn
|
|
1130
|
+
* of claude's own is running); everything else is a background turn, e.g.
|
|
1131
|
+
* claude answering a finished subagent's notification between user turns.
|
|
1132
|
+
*/
|
|
1133
|
+
async dispatchLiveMessage(liveQuery, message) {
|
|
1134
|
+
const turn = this.currentTurn?.liveQuery === liveQuery && this.currentTurn.settle ? this.currentTurn : null;
|
|
1135
|
+
const backgroundTurn = liveQuery.backgroundTurn;
|
|
1136
|
+
if (message?.type !== "result") {
|
|
1137
|
+
const target = backgroundTurn || turn || (liveQuery.backgroundTurn = this.createTurnState());
|
|
1138
|
+
await this.handleSdkMessage(message, target, { onProgress: target.onProgress });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
liveQuery.backgroundTurn = null;
|
|
1142
|
+
const uuids = resultUserMessageUuids(message);
|
|
1143
|
+
if (turn && (uuids.length ? uuids.includes(turn.uuid) : !backgroundTurn)) {
|
|
1144
|
+
// A background turn that folded the user's message in ends as this turn.
|
|
1145
|
+
if (backgroundTurn?.emittedAssistantMessage) {
|
|
1146
|
+
turn.emittedAssistantMessage = true;
|
|
1147
|
+
}
|
|
1148
|
+
await this.handleSdkMessage(message, turn, { onProgress: turn.onProgress });
|
|
1149
|
+
liveQuery.usageBase = modelUsageTotal(message) || liveQuery.usageBase;
|
|
1150
|
+
this.settleTurn(turn, null, message);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const target = backgroundTurn || this.createTurnState();
|
|
1154
|
+
await this.handleSdkMessage(message, target, {});
|
|
1155
|
+
await this.finishBackgroundTurn(target, message);
|
|
1156
|
+
}
|
|
1157
|
+
async finishBackgroundTurn(backgroundTurn, resultMessage, failure = null) {
|
|
1158
|
+
const failed = Boolean(failure) || (resultMessage && resultMessage.subtype !== "success");
|
|
1159
|
+
const text = normalizeText(resultMessage?.result);
|
|
1160
|
+
if (!failed && text && !backgroundTurn.emittedAssistantMessage) {
|
|
1161
|
+
await this.emitAssistantMessage(text);
|
|
1162
|
+
}
|
|
1163
|
+
// A pending user turn owns the working status.
|
|
1164
|
+
if (this.currentTurn) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
await this.emitTerminalWorkingStatus(backgroundTurn, failed
|
|
1168
|
+
? {
|
|
1169
|
+
phase: "turn_failed",
|
|
1170
|
+
status_done_line: failure?.message || extractResultErrorMessage(resultMessage),
|
|
1171
|
+
}
|
|
1172
|
+
: { phase: "turn_completed", status_done_line: "claude finished" });
|
|
1173
|
+
}
|
|
916
1174
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
|
|
917
1175
|
if (this.closeRequested) {
|
|
918
1176
|
throw this.createSessionClosedError();
|
|
@@ -942,31 +1200,24 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
942
1200
|
if (!suppressReply) {
|
|
943
1201
|
this.history.push({ role: "user", content: promptText });
|
|
944
1202
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
conversationReset: "",
|
|
952
|
-
emittedAssistantMessage: false,
|
|
953
|
-
fullText: "",
|
|
954
|
-
items: [],
|
|
955
|
-
query: null,
|
|
956
|
-
resultMessage: null,
|
|
957
|
-
terminalWorkingStatusEmitted: false,
|
|
958
|
-
usageByMessageId: new Map(),
|
|
959
|
-
};
|
|
1203
|
+
// Structured output is fixed when the process starts, so a jsonSchema turn
|
|
1204
|
+
// runs on a one-shot query of its own.
|
|
1205
|
+
const oneShot = Boolean(jsonSchema && typeof jsonSchema === "object");
|
|
1206
|
+
const abortController = oneShot ? new AbortController() : null;
|
|
1207
|
+
const currentTurn = this.createTurnState({ suppressReply, onProgress });
|
|
1208
|
+
currentTurn.abortController = abortController;
|
|
960
1209
|
this.currentTurn = currentTurn;
|
|
961
1210
|
this.markTurnStartedStatus();
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
abortController
|
|
1211
|
+
const stopTurn = () => {
|
|
1212
|
+
if (currentTurn.liveQuery) {
|
|
1213
|
+
this.closeLiveQuery(currentTurn.liveQuery);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
abortController?.abort();
|
|
968
1217
|
currentTurn.query?.close?.();
|
|
969
|
-
}
|
|
1218
|
+
};
|
|
1219
|
+
const closeGuard = this.createCloseGuard(stopTurn);
|
|
1220
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard(stopTurn);
|
|
970
1221
|
const previousOutputFormat = this.options.outputFormat;
|
|
971
1222
|
if (jsonSchema && typeof jsonSchema === "object") {
|
|
972
1223
|
this.options.outputFormat = { type: "json_schema", schema: jsonSchema };
|
|
@@ -977,21 +1228,35 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
977
1228
|
reply_in_progress: true,
|
|
978
1229
|
status_line: "claude is working",
|
|
979
1230
|
}, onProgress);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1231
|
+
let turnResult;
|
|
1232
|
+
if (oneShot) {
|
|
1233
|
+
const query = sdkModule.query({
|
|
1234
|
+
prompt: media.length ? buildClaudeInput(effectivePrompt, media) : effectivePrompt,
|
|
1235
|
+
options: this.buildSdkOptions(abortController),
|
|
1236
|
+
});
|
|
1237
|
+
currentTurn.query = query;
|
|
1238
|
+
turnResult = (async () => {
|
|
987
1239
|
for await (const message of query) {
|
|
988
1240
|
await this.handleSdkMessage(message, currentTurn, { onProgress });
|
|
989
1241
|
}
|
|
990
1242
|
return currentTurn.resultMessage;
|
|
991
|
-
})()
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1243
|
+
})();
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
turnResult = new Promise((resolve, reject) => {
|
|
1247
|
+
currentTurn.settle = (error, message) => (error ? reject(error) : resolve(message));
|
|
1248
|
+
});
|
|
1249
|
+
const liveQuery = this.ensureLiveQuery(sdkModule);
|
|
1250
|
+
currentTurn.liveQuery = liveQuery;
|
|
1251
|
+
currentTurn.query = liveQuery.query;
|
|
1252
|
+
currentTurn.usageBase = liveQuery.usageBase;
|
|
1253
|
+
currentTurn.uuid = randomUUID();
|
|
1254
|
+
if (!liveQuery.input.push(buildClaudeUserMessage(effectivePrompt, media, currentTurn.uuid))) {
|
|
1255
|
+
this.settleTurn(currentTurn, createTurnError("Claude process input is closed", { reason: "process_exited" }));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
turnResult.catch(() => { });
|
|
1259
|
+
const resultMessage = await Promise.race([turnResult, closeGuard.promise, turnTimeoutGuard.promise]);
|
|
995
1260
|
if (!resultMessage) {
|
|
996
1261
|
throw createTurnError("Claude query ended without a result message", {
|
|
997
1262
|
reason: "missing_result",
|
|
@@ -1012,7 +1277,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1012
1277
|
? [...resultMessage.permission_denials]
|
|
1013
1278
|
: [],
|
|
1014
1279
|
// A failed turn still spent tokens.
|
|
1015
|
-
usage: resultUsage(resultMessage),
|
|
1280
|
+
usage: resultUsage(resultMessage, currentTurn.usageBase),
|
|
1016
1281
|
});
|
|
1017
1282
|
}
|
|
1018
1283
|
const structuredOutput = jsonSchema && resultMessage.structured_output && typeof resultMessage.structured_output === "object"
|
|
@@ -1037,7 +1302,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1037
1302
|
}, onProgress);
|
|
1038
1303
|
return {
|
|
1039
1304
|
text: responseText,
|
|
1040
|
-
usage: resultUsage(resultMessage),
|
|
1305
|
+
usage: resultUsage(resultMessage, currentTurn.usageBase),
|
|
1041
1306
|
items: currentTurn.items,
|
|
1042
1307
|
events: [],
|
|
1043
1308
|
provider: this.backend,
|
|
@@ -1059,7 +1324,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1059
1324
|
// A failed turn still spent tokens; an interrupted query ends without a
|
|
1060
1325
|
// (final) result, so fall back to the last result plus what streamed since.
|
|
1061
1326
|
const usage = error?.usage ??
|
|
1062
|
-
sumStreamedUsage(currentTurn.usageByMessageId, currentTurn.resultMessage ? resultUsage(currentTurn.resultMessage) : null);
|
|
1327
|
+
sumStreamedUsage(currentTurn.usageByMessageId, currentTurn.resultMessage ? resultUsage(currentTurn.resultMessage, currentTurn.usageBase) : null);
|
|
1063
1328
|
if (error?.reason === "turn_timeout") {
|
|
1064
1329
|
await this.interruptCurrentTurn();
|
|
1065
1330
|
}
|
|
@@ -1094,11 +1359,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1094
1359
|
}
|
|
1095
1360
|
closeGuard.cleanup();
|
|
1096
1361
|
turnTimeoutGuard.cleanup();
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1362
|
+
currentTurn.settle = null;
|
|
1363
|
+
if (oneShot) {
|
|
1364
|
+
try {
|
|
1365
|
+
currentTurn.query?.close?.();
|
|
1366
|
+
}
|
|
1367
|
+
catch {
|
|
1368
|
+
// best effort
|
|
1369
|
+
}
|
|
1102
1370
|
}
|
|
1103
1371
|
}
|
|
1104
1372
|
}
|
|
@@ -1272,7 +1540,12 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1272
1540
|
}
|
|
1273
1541
|
this.closeRequested = true;
|
|
1274
1542
|
this.flushCloseWaiters();
|
|
1275
|
-
|
|
1543
|
+
if (!this.currentTurn?.liveQuery) {
|
|
1544
|
+
await this.interruptCurrentTurn();
|
|
1545
|
+
}
|
|
1546
|
+
if (this.liveQuery) {
|
|
1547
|
+
this.closeLiveQuery(this.liveQuery);
|
|
1548
|
+
}
|
|
1276
1549
|
this.closed = true;
|
|
1277
1550
|
}
|
|
1278
1551
|
}
|
|
@@ -157,6 +157,13 @@ function quoteShellArg(value) {
|
|
|
157
157
|
}
|
|
158
158
|
return `'${normalized.replace(/'/g, `'\\''`)}'`;
|
|
159
159
|
}
|
|
160
|
+
// Kimi Code reports a detached launch (`Agent`/`Bash` with run_in_background)
|
|
161
|
+
// with these metadata lines; the task's completion later wakes the main agent
|
|
162
|
+
// inside the same `kimi -p` process.
|
|
163
|
+
function isBackgroundTaskLaunch(content) {
|
|
164
|
+
const text = normalizeTextContent(content);
|
|
165
|
+
return /^task_id: /m.test(text) && /^automatic_notification: true$/m.test(text);
|
|
166
|
+
}
|
|
160
167
|
function normalizeTextContent(content) {
|
|
161
168
|
if (typeof content === "string") {
|
|
162
169
|
return content;
|
|
@@ -633,7 +640,9 @@ export class KimiPrintSession extends EventEmitter {
|
|
|
633
640
|
if (currentTurn.settled) {
|
|
634
641
|
return;
|
|
635
642
|
}
|
|
636
|
-
|
|
643
|
+
// `kimi -p` stays silent while it waits for background tasks it
|
|
644
|
+
// launched; killing it on idle would kill those tasks too.
|
|
645
|
+
if (Date.now() - lastActivityAt < this.turnDeadlineMs || currentTurn.backgroundTaskLaunched) {
|
|
637
646
|
scheduleTurnDeadline();
|
|
638
647
|
return;
|
|
639
648
|
}
|
|
@@ -700,6 +709,10 @@ export class KimiPrintSession extends EventEmitter {
|
|
|
700
709
|
}
|
|
701
710
|
if (role === "tool") {
|
|
702
711
|
noteToolFinished(currentTurn, payload.tool_call_id);
|
|
712
|
+
if (!currentTurn.backgroundTaskLaunched && isBackgroundTaskLaunch(payload.content)) {
|
|
713
|
+
currentTurn.backgroundTaskLaunched = true;
|
|
714
|
+
this.trace("background task launched; turn idle deadline suspended until kimi exits");
|
|
715
|
+
}
|
|
703
716
|
void this.emitWorkingStatus({
|
|
704
717
|
phase: "command_execution",
|
|
705
718
|
reply_in_progress: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"zod": "^4.1.5"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@love-moon/chat-web": "^0.
|
|
56
|
+
"@love-moon/chat-web": "^0.17.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.10.2",
|
|
60
60
|
"tsx": "^4.20.6",
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
|
-
"gitCommitId": "
|
|
63
|
+
"gitCommitId": "2799bc0"
|
|
64
64
|
}
|