@nowcrew/daemon 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/attachments.js +196 -0
- package/dist/computer-cli.js +72 -12
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +189 -20
- package/dist/config.js +2 -1
- package/dist/console.js +175 -9
- package/dist/execution-event-limit.js +1 -1
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +21 -1
- package/dist/execution-recovery.js +71 -0
- package/dist/execution-runner.js +68 -77
- package/dist/execution-supervisor.js +79 -31
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +5 -5
- package/dist/list-models.js +41 -5
- package/dist/local-executor.js +103 -14
- package/dist/machine-info.js +6 -1
- package/dist/main.js +23 -8
- package/dist/origin-decision.js +3 -1
- package/dist/prompt.js +4 -1
- package/dist/runner.js +14 -9
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +38 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +9 -4
- package/dist/runtimes/codex-app-server-runner.js +340 -0
- package/dist/runtimes/codex.js +10 -4
- package/dist/runtimes/kimi-acp-runner.js +117 -17
- package/dist/runtimes/kimi.js +2 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +212 -212
- package/dist/session.js +1 -1
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/slog.js +34 -20
- package/dist/supervised-runtime.js +104 -0
- package/dist/websocket-shutdown.js +53 -0
- package/package.json +3 -3
|
@@ -4,6 +4,9 @@ import { Readable, Writable } from "node:stream";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
6
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
7
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
8
|
+
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
9
|
+
import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
|
|
7
10
|
const ERROR_MESSAGE_CAP = 2_000;
|
|
8
11
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
9
12
|
function jsonLine(event) {
|
|
@@ -60,6 +63,17 @@ function safeErrorMessage(error, prompt) {
|
|
|
60
63
|
const redacted = prompt && raw.includes(prompt) ? raw.replaceAll(prompt, "[prompt redacted]") : raw;
|
|
61
64
|
return redacted.slice(0, ERROR_MESSAGE_CAP);
|
|
62
65
|
}
|
|
66
|
+
export function isKimiAuthenticationRequired(error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
return /\bauthentication required\b/i.test(message);
|
|
69
|
+
}
|
|
70
|
+
export function kimiResumeMethod(capabilities) {
|
|
71
|
+
if (capabilities?.sessionCapabilities?.resume != null)
|
|
72
|
+
return "resume";
|
|
73
|
+
if (capabilities?.loadSession)
|
|
74
|
+
return "load";
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
63
77
|
/** Full access may approve an operation, but it must never fabricate an answer to an agent question. */
|
|
64
78
|
export function selectKimiPermission(params) {
|
|
65
79
|
const allowOnce = params.options.filter((option) => option.kind === "allow_once");
|
|
@@ -100,11 +114,12 @@ async function stopChild(child) {
|
|
|
100
114
|
await closed;
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
|
-
/**
|
|
117
|
+
/** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
|
|
104
118
|
export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
105
119
|
const child = spawnProcess(options.bin, ["acp"], {
|
|
106
120
|
cwd: process.cwd(),
|
|
107
|
-
|
|
121
|
+
// probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
|
|
122
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
108
123
|
stdio: ["pipe", "pipe", "pipe"],
|
|
109
124
|
});
|
|
110
125
|
if (child.stdin === null || child.stdout === null || child.stderr === null)
|
|
@@ -119,15 +134,11 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
119
134
|
try {
|
|
120
135
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
121
136
|
const connected = app.connectWith(stream, async (context) => {
|
|
122
|
-
|
|
137
|
+
await context.request(methods.agent.initialize, {
|
|
123
138
|
protocolVersion: PROTOCOL_VERSION,
|
|
124
139
|
clientCapabilities: {},
|
|
125
140
|
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
126
141
|
});
|
|
127
|
-
const authMethod = initialized.authMethods?.[0];
|
|
128
|
-
if (authMethod !== undefined) {
|
|
129
|
-
await context.request(methods.agent.authenticate, { methodId: authMethod.id });
|
|
130
|
-
}
|
|
131
142
|
return true;
|
|
132
143
|
});
|
|
133
144
|
const result = await Promise.race([
|
|
@@ -157,12 +168,15 @@ export async function runKimiAcp(options) {
|
|
|
157
168
|
env: process.env,
|
|
158
169
|
stdio: ["pipe", "pipe", "pipe"],
|
|
159
170
|
});
|
|
171
|
+
let runtimeChild = child;
|
|
160
172
|
if (child.stdin === null || child.stdout === null || child.stderr === null) {
|
|
161
173
|
throw new Error("Kimi ACP process did not expose stdio");
|
|
162
174
|
}
|
|
163
175
|
child.stderr.pipe(process.stderr, { end: false });
|
|
164
176
|
let context = null;
|
|
165
177
|
let sessionId = null;
|
|
178
|
+
let acpSemanticProgress = false;
|
|
179
|
+
let progressTimedOut = false;
|
|
166
180
|
let cancelling = false;
|
|
167
181
|
const cancel = async () => {
|
|
168
182
|
if (cancelling)
|
|
@@ -171,7 +185,7 @@ export async function runKimiAcp(options) {
|
|
|
171
185
|
if (context !== null && sessionId !== null) {
|
|
172
186
|
await context.notify(methods.agent.session.cancel, { sessionId }).catch(() => undefined);
|
|
173
187
|
}
|
|
174
|
-
await stopChild(
|
|
188
|
+
await stopChild(runtimeChild);
|
|
175
189
|
};
|
|
176
190
|
const onSignal = () => {
|
|
177
191
|
void cancel().finally(() => process.exit(130));
|
|
@@ -181,9 +195,13 @@ export async function runKimiAcp(options) {
|
|
|
181
195
|
const app = client({ name: "nowcrew-daemon-kimi" })
|
|
182
196
|
.onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
|
|
183
197
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
198
|
+
acpSemanticProgress = true;
|
|
199
|
+
firstProgress.observe();
|
|
184
200
|
for (const event of mapKimiAcpUpdate(params.update))
|
|
185
201
|
await jsonLine(event);
|
|
186
202
|
});
|
|
203
|
+
let firstProgress = startFirstProgressWatchdog(() => undefined);
|
|
204
|
+
firstProgress.stop();
|
|
187
205
|
try {
|
|
188
206
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
189
207
|
const result = await app.connectWith(stream, async (nextContext) => {
|
|
@@ -196,15 +214,35 @@ export async function runKimiAcp(options) {
|
|
|
196
214
|
if (process.env.CREW_KIMI_ACP_DEBUG === "1") {
|
|
197
215
|
process.stderr.write(`Kimi ACP auth methods: ${JSON.stringify(initialized.authMethods ?? [])}\n`);
|
|
198
216
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
217
|
+
if (options.resume && options.sessionId) {
|
|
218
|
+
const resumeMethod = kimiResumeMethod(initialized.agentCapabilities);
|
|
219
|
+
if (resumeMethod === "resume") {
|
|
220
|
+
await nextContext.request(methods.agent.session.resume, {
|
|
221
|
+
sessionId: options.sessionId,
|
|
222
|
+
cwd: process.cwd(),
|
|
223
|
+
mcpServers: [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (resumeMethod === "load") {
|
|
227
|
+
await nextContext.request(methods.agent.session.load, {
|
|
228
|
+
sessionId: options.sessionId,
|
|
229
|
+
cwd: process.cwd(),
|
|
230
|
+
mcpServers: [],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
throw new Error("Kimi ACP does not advertise session resume support");
|
|
235
|
+
}
|
|
236
|
+
sessionId = options.sessionId;
|
|
202
237
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
else {
|
|
239
|
+
const session = await nextContext.request(methods.agent.session.new, {
|
|
240
|
+
cwd: process.cwd(),
|
|
241
|
+
mcpServers: [],
|
|
242
|
+
});
|
|
243
|
+
sessionId = session.sessionId;
|
|
244
|
+
}
|
|
245
|
+
await jsonLine({ type: "thread.started", thread_id: sessionId });
|
|
208
246
|
if (options.model) {
|
|
209
247
|
await nextContext.request(methods.agent.session.setConfigOption, {
|
|
210
248
|
sessionId,
|
|
@@ -212,11 +250,20 @@ export async function runKimiAcp(options) {
|
|
|
212
250
|
value: options.model,
|
|
213
251
|
});
|
|
214
252
|
}
|
|
253
|
+
firstProgress = startFirstProgressWatchdog(() => {
|
|
254
|
+
progressTimedOut = true;
|
|
255
|
+
void cancel();
|
|
256
|
+
});
|
|
215
257
|
return nextContext.request(methods.agent.session.prompt, {
|
|
216
258
|
sessionId,
|
|
217
259
|
prompt: [{ type: "text", text: prompt }],
|
|
218
260
|
});
|
|
219
261
|
});
|
|
262
|
+
firstProgress.stop();
|
|
263
|
+
if (progressTimedOut) {
|
|
264
|
+
process.stderr.write("Kimi produced no semantic progress within the startup window\n");
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
220
267
|
const usage = result.usage;
|
|
221
268
|
await jsonLine({
|
|
222
269
|
type: "turn.completed",
|
|
@@ -233,10 +280,54 @@ export async function runKimiAcp(options) {
|
|
|
233
280
|
return result.stopReason === "end_turn" ? 0 : result.stopReason === "cancelled" ? 130 : 1;
|
|
234
281
|
}
|
|
235
282
|
catch (error) {
|
|
283
|
+
if (progressTimedOut) {
|
|
284
|
+
process.stderr.write("Kimi produced no semantic progress within the startup window\n");
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
if (!acpSemanticProgress && isKimiAuthenticationRequired(error)) {
|
|
288
|
+
firstProgress.stop();
|
|
289
|
+
await stopChild(child);
|
|
290
|
+
process.stderr.write("Kimi ACP requires account login; falling back to configured CLI provider transport\n");
|
|
291
|
+
assertKimiLegacyPromptFits(prompt);
|
|
292
|
+
const fallback = spawn(options.bin, buildKimiArgs({
|
|
293
|
+
wakePrompt: prompt,
|
|
294
|
+
effectivePermission: "full_access",
|
|
295
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
296
|
+
...(options.resume && options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
297
|
+
}), {
|
|
298
|
+
cwd: process.cwd(),
|
|
299
|
+
env: process.env,
|
|
300
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
301
|
+
});
|
|
302
|
+
runtimeChild = fallback;
|
|
303
|
+
if (fallback.stdout === null || fallback.stderr === null) {
|
|
304
|
+
throw new Error("Kimi CLI fallback did not expose output streams");
|
|
305
|
+
}
|
|
306
|
+
firstProgress = startFirstProgressWatchdog(() => {
|
|
307
|
+
progressTimedOut = true;
|
|
308
|
+
void stopChild(fallback);
|
|
309
|
+
});
|
|
310
|
+
fallback.stdout.on("data", () => firstProgress.observe());
|
|
311
|
+
fallback.stdout.pipe(process.stdout, { end: false });
|
|
312
|
+
fallback.stderr.pipe(process.stderr, { end: false });
|
|
313
|
+
const code = await new Promise((resolve, reject) => {
|
|
314
|
+
fallback.once("error", reject);
|
|
315
|
+
fallback.once("close", (exitCode, signal) => {
|
|
316
|
+
resolve(exitCode ?? (signal === null ? 1 : 128));
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
firstProgress.stop();
|
|
320
|
+
if (progressTimedOut) {
|
|
321
|
+
process.stderr.write("Kimi CLI fallback produced no output within the startup window\n");
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
return code;
|
|
325
|
+
}
|
|
236
326
|
process.stderr.write(`Kimi ACP execution failed: ${safeErrorMessage(error, prompt)}\n`);
|
|
237
327
|
return 1;
|
|
238
328
|
}
|
|
239
329
|
finally {
|
|
330
|
+
firstProgress.stop();
|
|
240
331
|
process.off("SIGTERM", onSignal);
|
|
241
332
|
process.off("SIGINT", onSignal);
|
|
242
333
|
await stopChild(child);
|
|
@@ -248,11 +339,20 @@ function optionsFromArgv(argv) {
|
|
|
248
339
|
options: {
|
|
249
340
|
bin: { type: "string" },
|
|
250
341
|
model: { type: "string" },
|
|
342
|
+
session: { type: "string" },
|
|
343
|
+
resume: { type: "boolean", default: false },
|
|
251
344
|
},
|
|
252
345
|
});
|
|
253
346
|
if (!values.bin)
|
|
254
347
|
throw new Error("--bin is required");
|
|
255
|
-
|
|
348
|
+
if (values.resume && !values.session)
|
|
349
|
+
throw new Error("--resume requires --session");
|
|
350
|
+
return {
|
|
351
|
+
bin: values.bin,
|
|
352
|
+
...(values.model ? { model: values.model } : {}),
|
|
353
|
+
...(values.session ? { sessionId: values.session } : {}),
|
|
354
|
+
...(values.resume ? { resume: true } : {}),
|
|
355
|
+
};
|
|
256
356
|
}
|
|
257
357
|
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
258
358
|
runKimiAcp(optionsFromArgv(process.argv.slice(2)))
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -29,6 +29,8 @@ export function buildKimiArgs(input) {
|
|
|
29
29
|
const args = ["--output-format", "stream-json"];
|
|
30
30
|
if (input.model)
|
|
31
31
|
args.push("--model", input.model);
|
|
32
|
+
if (input.sessionId)
|
|
33
|
+
args.push("--session", input.sessionId);
|
|
32
34
|
args.push("--prompt", input.wakePrompt);
|
|
33
35
|
return args;
|
|
34
36
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const DEFAULT_FIRST_PROGRESS_TIMEOUT_MS = 120_000;
|
|
2
|
+
/**
|
|
3
|
+
* Bound the silent gap after a protocol turn starts. Once any semantic notification arrives,
|
|
4
|
+
* the runtime's configured total timeout remains authoritative; long-running tools are not killed
|
|
5
|
+
* merely because they produce no output.
|
|
6
|
+
*/
|
|
7
|
+
export function startFirstProgressWatchdog(onTimeout, timeoutMs = DEFAULT_FIRST_PROGRESS_TIMEOUT_MS) {
|
|
8
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
9
|
+
throw new RangeError("First-progress timeout must be a positive finite number");
|
|
10
|
+
}
|
|
11
|
+
let active = true;
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
if (!active)
|
|
14
|
+
return;
|
|
15
|
+
active = false;
|
|
16
|
+
onTimeout();
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
timer.unref?.();
|
|
19
|
+
const stop = () => {
|
|
20
|
+
if (!active)
|
|
21
|
+
return;
|
|
22
|
+
active = false;
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
};
|
|
25
|
+
return { observe: stop, stop };
|
|
26
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { dslog, flushSlog } from "./slog.js";
|
|
2
|
+
function defaultDiagnose(diagnostic) {
|
|
3
|
+
const fields = {
|
|
4
|
+
signal: diagnostic.signal,
|
|
5
|
+
stage: diagnostic.stage,
|
|
6
|
+
active_execution_count: diagnostic.activeExecutionCount,
|
|
7
|
+
active_legacy_count: diagnostic.activeLegacyCount,
|
|
8
|
+
deadline_ms: diagnostic.deadlineMs,
|
|
9
|
+
...(diagnostic.error === undefined ? {} : { error: diagnostic.error }),
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
dslog(diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown", `daemon shutdown ${diagnostic.stage}`, { level: diagnostic.stage === "failed" ? "ERROR" : "INFO", ...fields });
|
|
13
|
+
process.stderr.write(`${JSON.stringify({
|
|
14
|
+
level: diagnostic.stage === "failed" ? "ERROR" : "INFO",
|
|
15
|
+
event_type: diagnostic.stage === "failed" ? "daemon.shutdown_failed" : "daemon.shutdown",
|
|
16
|
+
...fields,
|
|
17
|
+
})}\n`);
|
|
18
|
+
}
|
|
19
|
+
catch { /* shutdown diagnostics must never interrupt cleanup */ }
|
|
20
|
+
}
|
|
21
|
+
export async function runServeLifecycle(service, dependencies = {}) {
|
|
22
|
+
const signals = dependencies.signals ?? process;
|
|
23
|
+
const flush = dependencies.flush ?? flushSlog;
|
|
24
|
+
const diagnose = dependencies.diagnose ?? defaultDiagnose;
|
|
25
|
+
let receivedSignal = null;
|
|
26
|
+
let resolveSignal;
|
|
27
|
+
const signalReceived = new Promise((resolve) => { resolveSignal = resolve; });
|
|
28
|
+
const receive = (signal) => {
|
|
29
|
+
if (receivedSignal !== null)
|
|
30
|
+
return;
|
|
31
|
+
receivedSignal = signal;
|
|
32
|
+
resolveSignal(signal);
|
|
33
|
+
};
|
|
34
|
+
const onSigint = () => receive("SIGINT");
|
|
35
|
+
const onSigterm = () => receive("SIGTERM");
|
|
36
|
+
signals.on("SIGINT", onSigint);
|
|
37
|
+
signals.on("SIGTERM", onSigterm);
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
await service.ready;
|
|
41
|
+
}
|
|
42
|
+
catch (readyError) {
|
|
43
|
+
try {
|
|
44
|
+
await service.stop();
|
|
45
|
+
}
|
|
46
|
+
catch { /* preserve the readiness error */ }
|
|
47
|
+
try {
|
|
48
|
+
await flush();
|
|
49
|
+
}
|
|
50
|
+
catch { /* slog is best effort */ }
|
|
51
|
+
throw readyError;
|
|
52
|
+
}
|
|
53
|
+
const signal = await signalReceived;
|
|
54
|
+
const snapshot = service.shutdownSnapshot();
|
|
55
|
+
diagnose({ signal, stage: "stopping", ...snapshot });
|
|
56
|
+
let stopError;
|
|
57
|
+
try {
|
|
58
|
+
await service.stop();
|
|
59
|
+
diagnose({ signal, stage: "completed", ...snapshot });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
stopError = error;
|
|
63
|
+
diagnose({
|
|
64
|
+
signal,
|
|
65
|
+
stage: "failed",
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
...snapshot,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await flush();
|
|
72
|
+
}
|
|
73
|
+
catch { /* slog owns its own spool fallback */ }
|
|
74
|
+
if (stopError !== undefined)
|
|
75
|
+
throw stopError;
|
|
76
|
+
return { signal };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
signals.off("SIGINT", onSigint);
|
|
80
|
+
signals.off("SIGTERM", onSigterm);
|
|
81
|
+
}
|
|
82
|
+
}
|